Skip to content

Скриптинг

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


Shebang '#!'

В первой строке файла скрипта необходимо указать какую оболочку для выполнения сценария мы собираемся использовать: #!/usr/bin/env bash

Команды оболочки отделяются знаком перевода строки, комментарии выделяют знаком решётки (#).

Пример простого скрипта
#!/usr/bin/env bash

# This is a comment
echo "The current directory is:" $PWD
echo "Your user name is:" $USER

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

Установка разрешений

После создания файла сценария для него необходимо установить разрешения на выполнение.

Установка разрешения на выполнение
#!/usr/bin/env bash

# Делаем файл исполняемым:
chmod +x ./myscript

# Теперь его можно выполнить:
./myscript

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

Существуют два типа переменных, которые можно использовать в bash-скриптах:

  • Переменные среды. Храняться в окружении пользователя, записываются прописными буквами, обращаются через знак $: $USER, $HOME, $PWD
  • Пользовательские переменные. Задаются в теле скрипта, записываются строчными буквами, обращаются через знак $: $my_var. Подобные переменные хранят значение до тех пор, пока не завершится выполнение сценария.

Подстановка команд

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

Сделать это можно двумя способами:

  • С помощью значка обратного апострофа «`»
  • С помощью конструкции $( )
Пример подстановки команд
#!/usr/bin/env bash

my_dir=$(pwd)
my_user=`whoami`
echo $my_dir
echo $my_user

Математические операции

Для выполнения математических операций в файле скрипта можно использовать конструкцию вида $(( a + b )):

Пример использования математических операций
#!/usr/bin/env bash

var1=$(( 5 + 5 ))
echo $var1
var2=$(( $var1 * 2 ))
echo $var2

Управляющая конструкция if-then-else

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

Пример использования
#!/usr/bin/env bash

my_dir=$PWD 
if [[ -n "$my_dir" ]] && [[ -d "$my_dir" ]] # если значение переменной не пустое и является директорией
then cd "$my_dir" ; echo "cd to $my_dir" # тогда переходим в нее и выполняем какие-либо операции
else echo "No such dir" # в противном случае - выполняем другие операции
fi
Эта же конструкция в сокращенном виде:
#!/usr/bin/env bash

my_dir=$PWD 
[[ -n "$my_dir" ]] && [[ -d "$my_dir" ]] && { cd "$my_dir"; echo "cd to $my_dir"; } || { echo "No such dir"; }
Скрипт для проверки существования пользователя:
#!/usr/bin/env bash

user=root
if grep $user /etc/passwd
then echo "The user $user Exists"
else echo "There is no such user: $user"
fi

Циклы for

Оболочка bash поддерживает циклы for, которые позволяют организовывать перебор последовательностей значений. Вот какова базовая структура таких циклов:

Базовая структура цикла for
#!/usr/bin/env bash

for var in list # В каждой итерации цикла в переменную var будет записываться следующее значение из списка list.
do
# В первом проходе цикла, таким образом, будет задействовано первое значение из списка.
cmd1; cmd2
done
# Во втором — второе, и так далее — до тех пор, пока цикл не дойдёт до последнего элемента.
Простейший пример цикла, выводит на экран определенные значения:
for var in first second third fourth fifth
do
echo The  $var item
done

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

Инициализация цикла
file="myfile"
for var in $(cat $file) # команда cat читает содержимое файла.
# Полученный список значений передаётся в цикл и выводится на экран.
do
echo "$var"
done

Переменная IFS - разделитель полей

В вышеописанном примере каждым новым элементом списка будет считаться значение считанное после разделителя полей, переменной IFS (Internal Field Separator), по умолчанию это: пробел, знак табуляции, знак перевода строки. Если bash встречает в данных любой из этих символов, он считает, что перед ним — следующее самостоятельное значение списка.

Переменную среды можно временно изменить, например в качестве разделителя полей нужен только перевод строки: IFS=$'\n'

Пример с заменой IFS
#!/usr/bin/env bash

file="/etc/passwd"
# Данный скрипт построчно обрабатывает указанный файл.
IFS=$'\n'
for var in $(cat $file)
do
echo " $var" # можно применять более сложные команды
done

# Разделителями могут быть и другие символы. Например, символ : 
IFS=:

Обход файлов, содержащихся в директории

Один из самых распространённых вариантов использования циклов for в bash-скриптах заключается в обходе файлов, находящихся в некоей директории, и в обработке этих файлов.

Обход директории
#!/usr/bin/env bash

for file in /home/$USER/* # подстановочный знак «*» в конце адреса папки означаюет: «все файлы с любыми именами».
do
# При проверке условия в операторе if, мы заключаем имя переменной в кавычки. Сделано это потому что имя файла или папки может содержать пробелы.
if [ -d "$file" ]
then echo "$file is a directory"
elif [ -f "$file" ]
then echo "$file is a file"
fi
done

Циклы for с условием окончания

for (( начальное значение переменной ; условие окончания цикла; изменение переменной ))

Цикл с условием
for (( i=1; i <= 10; i++ ))
do
echo "number is $i"
done

Вложенные циклы

Обработка содержимого файла вложенными циклами
#!/usr/bin/env bash

# В этом скрипте два цикла. Первый проходится по строкам, используя в качестве разделителя знак перевода строки. 
# Внутренний занят разбором строк, поля которых разделены двоеточиями.
IFS=$'\n'
for entry in $(cat /etc/passwd)
do
echo "Values in $entry –"
IFS=:
for value in $entry
do
echo " $value"
done
done

Цикл while

Цикл while выполняется, пока условие истинно.

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

Синтаксис цикла while
while команда проверки условия
do
cmd1; cmd2
done

Цикл until

Цикл until выполняется, пока условие ложно.

Управление циклами

Команда break. Позволяет прервать выполнение любого цикла при выполнении определенного условия

Команда continue. Когда в теле цикла встречается эта команда, текущая итерация завершается досрочно и начинается следующая, при этом выхода из цикла не происходит.

Применение прирывания в цикле
for (( var1 = 1; var1 < 15; var1++ ))
do
if [ $var1 -gt 5 ] && [ $var1 -lt 10 ]
then continue
fi
if [ $var1 -eq 16 ]
then break
fi
echo "Iteration number: $var1"
done

Обработка вывода, выполняемого в цикле

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

Перенаправление в цикле
for (( a = 1; a < 10; a++ ))
do
echo "Number is $a"
done > myfile.txt
echo "finished."

Пример: поиск исполняемых файлов

Надо выяснить, какие именно исполняемые файлы доступны в системе, можно просканировать все папки, записанные в переменную окружения PATH

Поиск исполняемых файлов
#!/usr/bin/env bash

IFS=:
for folder in $PATH
do
echo "$folder:"
for file in $folder/*
do
if [ -x $file ]
then echo " $file"
fi
done
done

Позиционные параметры

Чтение параметров командной строки

Оболочка bash назначает специальным переменным, называемым позиционными параметрами, введённые при вызове скрипта параметры командной строки:

  • $0 — имя скрипта.
  • $1 — первый параметр.
  • $2 — второй параметр — и так далее, вплоть до переменной $9, в которую попадает девятый параметр.

Параметры командной строки разделяются пробелами.

Если скрипту надо больше девяти параметров, при обращении к ним номер в имени переменной надо заключать в фигурные скобки, например так: ${10}

Проверка параметров

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

Проверка наличия параметров
if [ -n "$1" ]
then
echo Hello $1.
else
echo "No parameters found. "
fi

Подсчёт параметров

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

Последний из переданных параметров можно вызвать с помощью переменной: ${!#}

Захват всех параметров командной строки

В некоторых случаях нужно захватить все параметры, переданные скрипту. Для этого можно воспользоваться переменными $* и $@. Обе они содержат все параметры командной строки, что делает возможным доступ к тому, что передано сценарию, без использования позиционных параметров.

  • Переменная $* содержит все параметры, введённые в командной строке, в виде единого «слова».

  • В переменной $@ параметры разбиты на отдельные «слова». Эти параметры можно перебирать в циклах.

Пример скрипта, который производит суммирование всех переданных ему папраметров:
#!/usr/bin/env bash

for arg in $@; do
total=$(( total + arg ))
done
echo The sum is $total

Сдвиг значений командных параметров shift

Когда вы используете эту команду, она, по умолчанию, сдвигает значения позиционных параметров влево. Например, значение переменной $3 становится значением переменной $2, значение $2 переходит в $1, а то, что было до этого в $1, теряется. Обратите внимание на то, что при этом значение переменной $0, содержащей имя скрипта, не меняется.

shift
#!/usr/bin/env bash

count=1
# Скрипт задействует цикл while, проверяя длину значения первого параметра. Когда длина станет равна нулю, происходит выход из цикла.
while [ -n "$1" ]; do
echo "Parameter #$count = $1"
count=$(( $count + 1 ))
# После проверки первого параметра и вывода его на экран, вызывается команда shift, которая сдвигает значения параметров на одну позицию.
shift
done

Ключи командной строки

Ключи командной строки обычно выглядят как буквы, перед которыми ставится тире. Они служат для управления сценариями.

Обычно применяются в условных операторах для ветвления кода. Если в скрипт передан один ключ - будет выполнен один код, если иной ключ - другой код.

Как различать ключи и параметры

Часто при написании bash-скриптов возникает ситуация, когда надо использовать и параметры командной строки, и ключи. Стандартный способ это сделать заключается в применении специальной последовательности символов, которая сообщает скрипту о том, когда заканчиваются ключи и начинаются обычные параметры.

Эта последовательность — двойное тире (--). Оболочка использует её для указания позиции, на которой заканчивается список ключей. После того, как скрипт обнаружит признак окончания ключей, то, что осталось, можно, не опасаясь ошибок, обрабатывать как параметры, а не как ключи.

Параметры и ключи
#!/usr/bin/env bash
# Этот сценарий использует команду break для прерывания цикла while при обнаружении в строке двойного тире. 
# скрипт, разбирая переданные ему данные, находит двойное тире, он завершает обработку ключей и считает всё, что ещё не обработано, параметрами.

while [ -n "$1" ]; do
case "$1" in
-a) echo "Found the -a option" ;;
-b) echo "Found the -b option";;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
done
count=1
for param in $@; do
echo "Parameter #$count: $param"
count=$(( $count + 1 ))
done

Использование стандартных ключей

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

Вот список этих ключей:

  • -a Вывести все объекты.
  • -c Произвести подсчёт.
  • -d Указать директорию.
  • -e Развернуть объект.
  • -f Указать файл, из которого нужно прочитать данные.
  • -h Вывести справку по команде.
  • -i Игнорировать регистр символов.
  • -l Выполнить полноформатный вывод данных.
  • -n Использовать неинтерактивный (пакетный) режим.
  • -o Позволяет указать файл, в который нужно перенаправить вывод.
  • -q Выполнить скрипт в quiet-режиме.
  • -r Обрабатывать папки и файлы рекурсивно.
  • -s Выполнить скрипт в silent-режиме.
  • -v Выполнить многословный вывод.
  • -x Исключить объект.
  • -y Ответить «yes» на все вопросы.

Ввод данных read

Получение данных от пользователя.

Иногда сценарии нуждаются в данных, которые пользователь должен ввести во время выполнения программы. Именно для этой цели в оболочке bash имеется команда read. Эта команда позволяет принимать введённые данные либо со стандартного ввода (с клавиатуры), либо используя другие дескрипторы файлов. После получения данных, эта команда помещает их в переменную:

Простой пример read
#!/usr/bin/env bash
# команда echo, которая выводит приглашение, вызывается с ключом -n. 
# Это приводит к тому, что в конце приглашения не выводится знак перевода строки,
# что позволяет пользователю скрипта вводить данные там же, где расположено приглашение, а не на следующей строке.
echo -n "Enter your name: "
read name
echo "Hello $name, welcome to my program."
При вызове read можно указывать и несколько переменных:
read -p "Enter your name: " first last
echo "Your data for $last, $first…"
Специальная переменная среды REPLY
#Если, вызвав read, не указывать переменную, данные, введённые пользователем, будут помещены в специальную переменную среды REPLY:

read -p "Enter your name: "
echo Hello $REPLY, welcome to my program.
read -t
#!/usr/bin/env bash
# Если скрипт должен продолжать выполнение независимо от того, введёт пользователь какие-то данные или нет, 
# вызывая команду read можно воспользоваться ключом -t.
# А именно, параметр ключа задаёт время ожидания ввода в секундах:

# Если данные не будут введены в течение 5 секунд, скрипт выполнит ветвь условного оператора else, выведя извинения.
if read -t 5 -p "Enter your name: " name
then
echo "Hello $name, welcome to my script"
else
echo "Sorry, too slow! "
fi

Ввод паролей

Иногда то, что вводит пользователь в ответ на вопрос скрипта, лучше на экране не показывать. Например, так обычно делают, запрашивая пароли. Ключ -s команды read предотвращает отображение на экране данных, вводимых с клавиатуры. На самом деле, данные выводятся, но команда read делает цвет текста таким же, как цвет фона.

read -s
#!/usr/bin/env bash

read -s -p "Enter your password: " pass
echo "Is your password really $pass? "

Потоки (дескрипторы) ввода-вывода-ошибки.

Стандартные файловые дескрипторы

В каком-то смысле, все в Linux - это файлы, в том числе потоки ввода-вывода любой работающей программы. ОС работает с файлами через файловые дескрипторы. Для любого linux-процесса зарезервированно минимум 3 дескриптора:

  • 0, STDIN — стандартный поток ввода.
  • 1, STDOUT — стандартный поток вывода.
  • 2, STDERR — стандартный поток ошибок.

Ввод и вывод данных в скрипте осуществляется через эти потоки.

stdin

Стандартный поток ввода shell, по умолчанию - ввод с клавиатуры в терминал. Поток ввода может быть переопределен оператором <. Например, при использовании команды read text < file.txt, на стандартный поток ввода команды read будет выведен FD файла file.txt и в переменную text будет записано содержание файла. Система читает файл и обрабатывает данные так, будто они введены с клавиатуры.

Многие команды bash принимают ввод из STDIN, если в командной строке не указан файл, из которого надо брать данные.

stdout

Стандартный поток вывода shell, по-умолчанию - это окно терминала. Данный поток может быть перенаправлен в другое место с помощью команд:

  • > - перезапишет файл; pwd > file.txt
  • >> - добавит данные в конец файла; whoami >> file.txt

stderr

Стандартный поток вывода ошибок исполняемых в оболочке команд, по умолчанию - это окно терминала. Ошибки могут быть перенаправленны с помощью команд:

  • 2> - перенаправление stderr в файл или в pipe. ls ~/user > ls_out.txt 2> ls_err.txt - перенаправит стандартный вывод и ошибки в разные файлы
  • &> - сокращение для совместного перенаправления stdout и stderr. ls ~/user1 &> ls_cmd.txt - перенаправит вывод команды ls и ошибки в файл ls_cmd.txt

Нотация >&

Для перенаправления вывода в указанный файловый дескриптор можно использовать нотацию >&:

  • >&1 - перенаправление вывода в stdout
  • >&2 - перенаправление вывода в stderr
  • >&3 - перенаправление вывода в другой дескриптор

exec - потоковое перенаправление ввода-вывода

Если в скрипте стоит задача постояенного перенаправления вывода, можно воспользоваться командой exec

exec
# Перенаправляем весь stdin скрипта 
exec 0< stdin_file.txt

# Перенаправляем весь stdout скрипта в отдельный файл
exec 1> stdout_file.txt

# Перенаправляем весь stderr скрипта в отдельный файл
exec 2> stderr_file.txt

# Перенаправляем определенный поток вывода скрипта в отдельный файл
exec 3> fd3_file.txt
echo "data to file descriptor number 3" >&3

Команду exec можно использовать в любом месте скрипта.

Закрытие fd

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

Принудительное закрытие fd
exec 3> fd3_file.txt # открыли дескриптор
echo "data for fd#3" >&3 # отправили в него данные
exec 3>&- # закрыли дескриптор
# Теперь нельзя передать вывод в этот дескриптор
echo "some data" >&3 # не получится

Будьте внимательны, закрывая дескрипторы файлов в сценариях. Если вы отправляли данные в файл, потом закрыли дескриптор, потом — открыли снова, оболочка заменит существующий файл новым. То есть всё то, что было записано в этот файл ранее, будет утеряно.

lsof - средство для анализа открытых fd

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

  • sudo lsof - показывает все дескрипторы открытые в ОС в данный момент
  • lsof -p PID - показывает дескрипторы для конкретного процесса
  • lsof -d 0,1,2 - показывает сведения для конкретных дескрипторах