Скрипты
Откройте терминал и введите команды по порядку:
$ echo '#!/bin/bash' > hi.sh
$ echo 'echo Hello there from Shell' >> hi.sh
$ chmod 755 hi.sh
$ ./hi.sh
Hello there from Shell
Вы только что написали свой первый скрипт.
Тут, однако, потребуется небольшое разъяснение. Linux не обращает внимание на расширения файлов, его, по сути, нет — это просто название файла. bash‑скрипты принято называть с .sh на конце, но оболочка понимает, что это скрипт, по‑другому признаку. В частности — по первой строчке, которую мы выполнили до этого. #! называется shebang. Он напрямую сообщает оболочке, какой интерпретатор использовать. Ранее мы приводили пример с утилитой deluser, которая де‑факто была скриптом, написанным на Perl.
$ sed 1q /usr/sbin/deluser
#! /usr/bin/perl
При запуске скрипта оболочка выберет правильный интерпретатор.
Shebang, который мы указали в самом начале — это прямой путь до интерпретатора. В некоторых дистрибутивах он может лежать в другом месте. В системе есть приложение, которое ищет исполняемый файл по PATH — /usr/bin/env. Для переносимости лучше использовать его.
#!/usr/bin/env bash
В случае с python утилита env сможет запустить приложение в виртуальном окружении, если оно имеется.
Аргументы
Скриптам можно передавать аргументы, как и любой другой команде. Они также называются позиционными параметрами и доступны через $1, $2, $3 и т. д. В $0 лежит путь до скрипта с его именем.
#!/usr/bin/env bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Total arguments: $#"
echo "All arguments: $@"
echo "All arguments as a string: $*"
Выполним скрипт:
$ ./script.sh a b
Script name: ./script.sh
First argument: a
Second argument: b
Total arguments: 2
All arguments: a b
Аргументы можно сдвигать ключевым словом shift, т. е. затереть предыдущее и сделать его текущим:
#!/usr/bin/env bash
while [ $# -gt 0 ]; do
echo "Argument: $1"
shift
done
Чтобы получить все аргументы в виде одной строки, можно использовать $*.
Вы также можете определить их значения по умолчанию:
#!/usr/bin/env bash
arg1=${1:-"default_arg1"}
arg2=${2:-"default_arg2"}
echo "First argument: $arg1"
echo "Second argument: $arg2"
Используя встроенную команду getopts, можно создавать именованные аргументы:
#!/usr/bin/env bash
while getopts "a:b:" opt; do
case $opt in
a) arg1="$OPTARG" ;;
b) arg2="$OPTARG" ;;
*) echo "Usage: $0 -a arg1 -b arg2"; exit 1 ;;
esac
done
echo "Argument a: $arg1"
echo "Argument b: $arg2"
Соответственно, вызов скрипта будет выглядеть так:
$ ./example.sh -a value1 -b value2
Регулярные выражения
Регулярные выражения — это мощный инструмент для поиска текста по шаблону. Мы уже частично обсуждали их в рамках утилиты grep. Оболочка bash позволяет работать с регулярными выражениями без вызова отдельной утилиты — через конструкцию [[ ... ]] и оператор =~.
Часть символов называются метасимволами — т. е. те, которые описывают сам шаблон. Вот их список: ^ и $ — совпадение с начала или конца строки; . — совпадает любой символ кроме перевода строки; [] — определяет набор символов или класс; {} — определяет количество символов, которые должны совпасть; - — определяет диапазон для совпадения; ? — делает предыдущий символ необязательным; *, + — совпадение от нуля или от одного шаблона соответственно; () — объединение нескольких выражений; | — логическое «или»; \ — экранирование метасимволов (например, тех же скобок).
Рассмотрим следующий пример. Информация по этой книге размещена по адресу https://highway.chrns.com/#about и пускай нам интересно, по какой‑то причине выделить домен из данной строки. Во‑первых, ссылка начинается с https; чтобы решение также работало для http, пометим последний символ как необязательный: https?://. Далее нас интересует всё до следующего знака / — это выражается как ([^/]+).
Результат будет храниться в массиве BASH_REMATCH. По индексу 0 хранится полное совпадение, все остальные — соответствуют совпадению в ().
#!/usr/bin/env bash
if [[ "$1" =~ ^https?://([^/]+) ]]; then
echo "Domain is ${BASH_REMATCH[1]}"
fi
Ещё пример:
#!/usr/bin/env bash
current_date="(date +\"Today: %Y-%m-%d\")"
if [[ $current_date =~ ([0-9]{4})-([0-9]{2})-([0-9]{2}) ]]; then
echo "Full match: ${BASH_REMATCH[0]}"
echo "Year: ${BASH_REMATCH[1]}"
echo "Month: ${BASH_REMATCH[2]}"
echo "Day: ${BASH_REMATCH[3]}"
fi
Функции
Как и в других языках программирования, в bash есть функции. Они позволяют переиспользовать код и не писать одно и то же много раз.
function add {
echo $(($1 + $2))
}
Все позиционные аргументы являются локальными и не пересекаются с аргументами скрипта. Если вам нужны аргументы скрипта внутри функции — передайте их в явном виде.
Запуск команды ничем не отличается от любой другой команды — просто вызовите func_name. К слову, параметры передаются точно так же, как и для любой другой команды.
$ add 1 2
3
Функции, однако, не возвращают значение как таковое — они возвращают код возврата. Чтобы вернуть его явным образом — используйте ключевое слово return:
function written_to_fail {
return 1
}
# ...
$ written_to_fail
$ echo $?
1
Любые переменные, что вы создаёте, всегда попадают в глобальную область видимости — т. е. будут доступны за пределами функции. Во‑первых, это не очень хорошо с точки зрения сокрытия данных, а во‑вторых, это засоряет окружение. Чтобы избежать «просачивания» переменных за пределы функции, используйте ключевое слово local при объявлении:
function test {
local var="some local var"
echo inside: $var
}
$ test
$ echo outside: $var
inside: some local var
outside:
Вас, к слову, никто не ограничивает в вызове функции самой этой функции, т. е. возможна рекурсия:
#!/usr/bin/env bash
factorial() {
if [ $1 -le 1 ]; then
echo 1
else
local prev=$(factorial $(($1 - 1)))
echo $(($1 * prev))
fi
}
echo "Factorial of 5 is $(factorial 5)"
Глубина рекурсии ограничена во‑первых, размером стека (ulimit -s) — если рекурсия съест всю память, то случится segmentation fault. Во‑вторых, в окружении есть переменнаяFUNCNEST, которая дополнительно ограничивает глубину рекурсии.
Хоть bash и не требует «точки входа» через специальную функцию, хорошая практика для больших скриптов вынести всю логику в функцию main и запустить её в конце скрипта. Это увеличит читаемость кода, поможет с декомпозицией и модульностью и поможет избежать неожиданных выполнений функций.
Если вы писали программы на python, то наверняка помните конструкцию вида:
if __name__ == '__main__':
# the best code is written here
В bash можно сделать почти так же:
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main
fi
В таком случае скрипт будет работать только если вы запустили его напрямую, а не через другой скрипт.
Перехват сигналов
Ранее мы уже обсуждали, что программы могут принимать сигналы от операционной системы — bash‑скрипты не исключение. Вы можете прервать его выполнение, например, нажав Ctrl+C.
Скрипт завершит работу, но иногда нужно выполнить какие-нибудь действия до аварийного завершения, например удалить временные файлы. Перехватить прерывание можно с помощью trap.
#!/usr/bin/env bash
trap 'echo "Ctrl+C pressed!"; exit 1' SIGINT
while true; do
sleep 1
done
В качестве аргументов можно передать список сигналов:
trap 'echo "Signal received. Exiting..."; exit' SIGINT SIGTERM SIGHUP
Как вы уже заметили, вам нужно вручную вызвать команду exit, т. е. сигналы можно проигнорировать:
trap '' SIGINT # ignore SIGINT (Ctrl+C)
Чтобы сбросить перехват по конкретному сигналу, нужно вызвать команду trap с указанием нужного вам сигнала без команды.
trap SIGINT # reset
С целью подчистки после себя проще всего создать отдельную функцию и выписать всю необходимую логику туда.
#!/usr/bin/env bash
cleanup() {
echo "Terminating child process..."
kill "$child_pid"
exit
}
trap cleanup SIGINT SIGTERM
# Launching a background process
(sleep 10) &
child_pid=$!
echo "Press Ctrl+C to terminate the script and the background process."
wait $child_pid
Вы можете создавать свои собственные пользовательские сигналы и отправлять их через kill.
Ветвление
Одна из самых важных конструкций любого языка программирования — ветвление. Здесь, как и в Си, используется ключевое слово if, однако в bash блок не обрамляется в фигурные скобки; условие завершается словом fi. Вы также можете создавать более сложные ветвления добавив ещё одно условие через elif и отловить все условия, которые не попали под предыдущие блоки через else.
if test-commands; then
consequent-commands;
[elif more-test-commands; then
more-consequents;]
[else alternate-consequents;]
fi
Примечание
Условный оператор
ifожидает получение0, а не1как в других языках программирования. Помните, что код возврата0означает, что команда выполнена успешно, без ошибок!
Приведём пример для иллюстрации:
if grep -q "pattern" file.txt; then
echo "Pattern found."
fi
Если хотя бы одно условие отработало, то в$?будет храниться код возврата последней команды; если же ни одно из условий не выполнится, то там будет храниться0.
Сравнение строк:
=,==— проверить, что строки эквивалентны!=— проверить, что строки не равны-z— проверить, пустая ли строка-n— проверить, что строка не пустая
Сравнение чисел (в скобках эквивалент в языке Си): -eq (==), -ne (!=), -gt (>), -lt (<), -ge (>=), -le (<=).
Проверка файлов: -e — существование файла; -f — проверка, что файл существует и он является обычным файлом (не символьная ссылка, не устройство и т. д.); -d — существует ли директория; -r — файл доступен для чтения; -w — можно писать; -x — можно исполнять.
Логические операции: &&, ||, ! (отрицание).
#!/usr/bin/env bash
number=42
if [ $number -eq 42 ]; then
echo "It's definitely 42!"
fi
$ ./script.sh
It's definitely 42!
В случае, когда требуется проверять совпадение с несколькими условиями сразу, то лучше использовать операторы case/esac:
#!/usr/bin/env bash
fruit="apple"
case $fruit in
"apple")
echo "It's an apple."
;;
"banana")
echo "It's a banana."
;;
"orange"|"lemon")
echo "It's a citrus fruit."
;;
*)
echo "Unknown fruit."
;;
esac
Можно использовать шаблоны (globs):
case $file in
*.txt)
echo "It's a text file."
;;
*)
echo "Unknown file type."
;;
esac
Или описывать шаблон:
#!/usr/bin/env bash
read -p "Enter yes or no: " input
case $input in
[Yy]|[Yy][Ee][Ss])
echo "You said yes."
;;
[Nn]|[Nn][Oo])
echo "You said no."
;;
*)
echo "Invalid input."
;;
esac
Для создания простого интерактивного меню можно использовать ключевое слово select. Ранее мы уже упоминали переменную PS3, её как раз можно использовать для создания текста‑приветствия:
#!/usr/bin/env bash
PS3="Choose your favorite color: "
select color in red green blue yellow quit; do
case $color in
red|green|blue|yellow)
echo "You chose $color."
;;
quit)
echo "Goodbye!"
break
;;
*)
echo "Invalid choice. Try again."
;;
esac
done
Пример вывода:
$ ./menu.sh
1) 700 nm
2) 555 nm
3) 460 nm
4) 580 nm
5) quit
Choose your favorite color: 1
You chose 700 nm.
Choose your favorite color: 6
Invalid choice. Try again.
Choose your favorite color: 5
Goodbye!
Можно использовать другую команду для генерации опций:
#!/usr/bin/env bash
PS3="Select a file to process: "
select file in $(ls); do
if [ -f "$file" ]; then
echo "You selected $file."
# Here we do something with the file
break
else
echo "Invalid selection. Please try again."
fi
done
Циклы
Оболочка bash поддерживает несколько разных видов циклов.
until — цикл будет выполняться, пока не выполнится условие в [ ]:
#!/usr/bin/env bash
cnt=0
until [ $cnt -gt 5 ]
do
echo Counter: $cnt
((cnt++))
done
При желании можно записать цикл в одну строчку, разделяя строки знаком ;:
$ until [ $cnt -gt 5 ] ; do echo Counter: $cnt; ((cnt++)); done
Другая форма цикла записывается через while, который выполняется пока верно условие:
while test-commands; do consequent-commands; done
И третья форма — через for, позволяет итеррироваться по списку:
#!/usr/bin/env bash
for number in {1..5}; do
echo "Number: $number"
done
Список может быть любым, можно вытащить его из переменной или вовсе пройтись по всем файлам в директории через /path/to/dir/*.
#!/usr/bin/env bash
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "User: $user"
done
Если вам по душе цикл for в стиле Си, то его тоже можно сконструировать:
#!/usr/bin/env bash
for ((i=0; i<5; i++)); do
echo "Index is $i"
done
Как и в Си, здесь имеются ключевые слова break и continue для управления циклом.
#!/usr/bin/env bash
for number in {1..10}; do
if [ $number -eq 5 ]; then
continue # Skipping 5
fi
if [ $number -eq 8 ]; then
break # Leaving at 8
fi
echo "Number: $number"
done
Обмен данными
Рассмотрим пример работы двух скриптов, обменивающихся данными между собой через именованный конвейер. Сначала создайте именованный конвейер: mkfifo mypipe.
#!/usr/bin/env bash
for i in {1..5}; do
echo "Message $i from Producer" > mypipe
sleep 1 # Simulating a delay
done
Клиент:
#!/usr/bin/env bash
while read line; do
echo "Consumer received: $line"
done < mypipe
Команда read также поддерживает тайм‑аут для ввода:
read -t 5 -p "Enter something (you have 5 seconds): " input
echo "You entered: $input"
Код возврата — 0, если всё ок, и 1 — если не успели. Можно данные сразу сохранять в переменные:
echo "Enter your first and last name:"
read first_name last_name
echo "First Name: $first_name"
echo "Last Name: $last_name"
Используя регулярные выражения, можно также валидировать вход:
while true; do
read -p "Enter a number: " num
if [[ "$num" =~ ^[0-9]+$ ]]; then
echo "You entered a valid number: $num"
break
else
echo "Invalid input. Try again."
fi
done
Если вы хотите ввести пароль, то ввод можно скрыть:
read -s -p "Enter your password: " password
echo
echo "Password saved!"
Команду read также можно использовать для создания массива:
echo "one two three" | read -a my_array
echo "First element: ${my_array[0]}"
echo "Second element: ${my_array[1]}"
Или использовать другой разделитель:
echo "A1:B2:C3" | read -d ':' first_part
echo "First part: $first_part"
С её помощью можно считывать файл построчно:
while read -r line; do
echo "Processing: $line"
done < file.txt
В Linux есть команда write, мы не будем её рассматривать — она нужна для передачи сообщений между пользователями системы.
Утилита set и отладка скрипта
У bash есть одна очень важная встроенная команда — она позволяет настраивать поведение оболочки, т. е. включать/отключать некоторые функции. Опций очень много, мы не будем рассматривать их все. Вы можете вывести список и статус каждой из них так:
$ set -o
# or:
$ shopt
Либо узнать о ни так:
$ echo $-
himBHs
Если нужно сбросить все настройки к тем, что были по умолчанию, то используйте команду set -.
Допустим, вы написали скрипт и сделали ошибку в названии какой-нибудь переменной. Такое часто бывает. Скорее всего вы не сразу обнаружите ошибку — придётся несколько раз запустить скрипт, чтобы осознать, что в каком-то определённом месте у вас вместо ожидаемого значения почему-то пустота. Через set вы можете заставить скрипт завершаться сразу, как только будет попытка обратиться к переменной, которая не задана:
#!/usr/bin/env bash
set -u
echo $variable
ehco "end"
Активация той или иной функции осуществляется символом-перед буквой означающей опцию. Отключение символом+.
Если запустить скрипт, то вы получите сообщение об ошибке, а последняя строчка где выводится слово end не будет распечатана, т. к. скрипт будет остановлен:
$ ./not_exist.sh
./not_exist.sh: line 2: variable: unbound variable
К слову, вы можете запускать сам bash с нужными ключами, т. е. можно не писать там set как одну из команд:
#!/bin/bash -u
echo $variable
ehco "end"
Точно так же можно заставить завершать работу скрипта, как только одна из команд вернёт ненулевой код возврата.
Также bash предоставляет инструмент отладки через опцию -x — дополнительная информация может настраиваться через ранее упомянутую переменную PS4.
#!/usr/bin/env bash
set -x # Enable debugging
echo "Let's try debug output!"
set +x # Disable debugging
По умолчанию в PS4 лежит +. В этом случае вывод скрипта выше будет иметь вид:
+ echo "Let's try debug output!"
Let's try debug output!
+ set +x
Однако можно выводить больше информации, задав PS4, например:
PS4='+ Line $LINENO: ' # Include the line number
PS4='+ ${BASH_SOURCE}:$LINENO: ' # Include script name and line number
PS4='+ ${FUNCNAME[0]}:$LINENO: ' # Include function name and line number
PS4='+ PID $$, Line $LINENO: ' # Include process ID and line number
