Автор: Шалита Суранга (Shalitha Suranga)
Современные операционные системы предлагают два механизма взаимодействия с пользователем: CLI и GUI.
Для взаимодействия с операционной системой через интерфейс командной строки ( CLI ), пользователь, с помощью клавиатуры, вводит различные команды в программу-терминал. Графический интерфейс пользователя ( GUI ) предполагает манипуляции пользователя указывающим устройством, например компьютерной мышкой. Большинство программистов предпочитают CLI-ориентированный подход из-за его гибкости, поддержки автоматизации, производительности и синтаксиса, подобного синтаксису языков программирования.
Сегодня Bash помогает каждому пользователю Unix/Unix-подобных операционных систем работать с терминалами, благодаря полнофункциональному командному языку. DevOps инженеры и программисты склонны выбирать Bash для написания скриптов автоматизации, сервисных скриптов и, даже, программ общего назначения. Знание малоизвестных приемов синтаксиса Bash, на самом деле, полезно для всех пользователей , и особенно для программистов.
В мире Bash, параметр — это некий элемент, который программисты могут использовать для хранения данных в процессе исполнения программы. В Bash есть три типа параметров — переменные, позиционные и специальные. Давайте рассмотрим поведение этих трех типов параметров на нескольких практических примерах.
Использование позиционных параметров в скриптах и функциях.
Существует несколько способов передачи дополнительных входных данных Bash-скрипту. Если мы пишем скрипт, который будет использоваться человеком, то можем воспользоваться встроенной командой read , чтобы получить данные удобным пользователю способом, считав их прямо с клавиатуры. Если мы создаем DevOps или сервисный скрипт, не требующий во время своего исполнения взаимодействия с пользователем, то для получения входных данных, мы можем использовать механизм передачи в скрипт аргументов командной строки. Позиционные параметры помогают нам получать доступ к списку переданных процессу аргументов командной строки в индексированном виде, наподобие использования argc в языке программирования C.
Посмотрите на следующий скрипт, демонстрирующий арифметическое сложение позиционных параметров, принявших значения первых двух аргументов командной строки ( предполагается, что переданные аргументы имеют тип integer)
#!/bin/bash
sum=$(($1 + $2))
echo "sum = $sum"
Если, в масштабах скрипта, $n ссылается на значение n-ого аргумента командной строки, то внутри функции эти позиционные параметры соответствуют уже аргументам, переданным этой конкретной функции. Рассмотрим фрагмент кода ниже:
#!/bin/bash
function sum() {
echo $(($1 + $2))
}
read -p "Введите num1: " n1
read -p "Введите num2: " n2
echo "sum = $(sum $n1 $n2)"
Хорошо, а что если мы не знаем точное количество аргументов , которое будет передано функции? В этом случае, мы можем обработать весь список аргументов целиком, либо перебирая его элементы с помощью $@ , либо получая доступ к каждому элементу через косвенное расширение, как показано ниже:
#!/bin/bash
function parse_with_for_in() {
for arg in "$@"
do
echo $arg
done
}
function parse_with_expansion() {
for ((i = 1; i <= $#; i++))
do
echo ${!i} # Синтаксис косвенного расширения
done
}
parse_with_for_in "$@"
echo "---"
parse_with_expansion "$@"
Первая функция содержит цикл for-in, перебирающий все аргументы функции. Вторая функция извлекает значения параметров по их индексам. Например, когда i =2, оператор косвенного расширения сначала создает параметр $2, а затем ищет значение второго аргумента функции.
Кстати, значение параметра $0 соответствует имени файла, содержащего скрипт. Мы можем разрабатывать полноценные CLI-программы с помощью Bash, добавляя оператор case внутрь цикла for, описанного выше.
Взаимодействие с дочерними процессами с помощью Специальных Параметров
Bash - это, и язык команд, и их интерпретатор для ОС Linux Shell, так что, в большинстве случаев, вы сможете писать скрипты не прибегая к порождению каких-либо дочерних процессов. Например, вы можете выполнять манипуляции со строками, обработку массивов и базовые арифметические операции используя только Bash, благодаря хорошо известному функционалу расширения параметров. Однако мы обычно используем Bash для создания служебных программ и сценариев DevOps, в этих случаях использование дочерних процессов неизбежно.
Запуская другие программы из Bash, мы можем использовать параметр $?, чтобы получить код завершения только что закончившегося процесса. Давайте выполним команду и посмотрим какой у нее код завершения.
Bash также разрешает запускать процессы в фоновом режиме используя символ & . А вот что, если понадобиться завершить такой фоновый процесс порожденный вами ранее ? Параметр $! возвращает идентификатор процесса ( PID ) только что запущенной фоновой задачи. Следующий скрипт запускает gedit на пять секунд:
#!/bin/bash
gedit &
sleep 5
kill $!
Если вы работаете с множественными фоновыми процессами — сохраняйте значение их $! параметров в переменных. Параметр $$ содержит идентификатор текущего процесса Bash. Внутри вложенных программных оболочек он всегда содержит идентификатор родительского Bash процесса. Некоторые старые Bash-скрипты используют $$ для создания уникальных временных файлов. Но такой способ является небезопасным, вследствие предсказуемости имен временных файлов для потенциального атакующего, так что используйте команду mktemp для создания защищенных временных файлов.
Предопределенные переменные, помогающие в отладке Bash скриптов.
Многие программисты используют команду echo в процессе отладки Bash скриптов, точно так же, как они используют, например, console.log для отладки JavaScript программ. Это, несомненно, очень удобно для быстрой разработки простых Bash скриптов. Но если ваш скрипт оперирует большим количеством данных и выполняет множество команд, то такой подход становится слишком затратным по времени.
Многие DevOps инженеры знают, что мы можем запускать командный интерпретатор Bash с флагом -x или, даже -xv , для отображения выполняющейся команды и текущего фрагмента кода, соответственно. По умолчанию, Bash будет показывать префикс + для выполняемой в настоящее время команды, как показано на изображении ниже:
Имейте ввиду: , чтобы включить функцию отладки для этого примера, я использовал следующий Шебанг(Shebang):
#!/bin/bash -x
Такое же поведение интерпретатора Bash можно получить, вызвавset -x в тексте скрипта.
Что бы сделать наш отладочный процесс еще более удобным, мы можем использовать встроенные локальные переменные PS4 и LINENO . Взгляните на приведенный ниже фрагмент:
#!/bin/bash
green='tput setaf 2'
reset='tput sgr0'
PS4='$($green)Line: $LINENO -> $($reset)'
set -x
a=10
b=15
echo "a = $a"
echo "b = $b"
В этом примере локальная переменная PS4 по умолчанию содержит символ + как префикс для указания исполняемой в настоящий момент команды. Но мы переопределили ее, чтобы она отображала номера строк в зеленом цвете, как показано на изображении ниже:
Если вам требуется произвести отладку Bash скрипта, который импортирует несколько исходных файлов, вы можете использовать локальную переменную BASH_SOURCE , для отображения имени текущего файла скрипта с помощью PS4. В Bash также имеется локальная переменная FUNCNAME для получения текущего стека вызовов функций, для поиска имени текущей функции можно использовать конструкцию ${FUNCNAME[0]} ( или просто $FUNCNAME).
Разделение строк, как в Python, с помощью переменной IFS
Создавая Bash скрипты, мне часто приходиться решать задачи по разделению строк. Например, в некоторых скриптах, нам может понадобиться обработать CSV файлы, разделяя каждую строку на фрагменты, ограниченные запятыми. Также, часто бывает нужно, используя команду read , захватить клавиатурный ввод и обработать его в соответствии с определенной маской ввода. Мы можем легко решить эти задачи с помощью IFS (Internal Field Separator — Внутренний Разделитель Полей). IFS — это внутренняя переменная, указывающая Bash каким образом разделять строки на фрагменты.
По умолчанию переменная IFS имеет значение <space><tab><newline>, но вы можете отредактировать ее в соответствии со своими потребностями, а затем, если понадобиться, вернуть ее к исходному значению. Давайте начнем с простого примера. Предположим, что вы хотите попросить пользователя ввести серийный номер в формате NN-NNNN и сохранить оба сегмента номера, разделенных дефисом, в разных переменных. В общем случае, команда read обрабатывает строки базируясь на значении переменной IFS, заданном по умолчанию, но мы можем изменить ее значение, как показано ниже:
#!/bin/bash
IFS=-
read -p "Serial number (i.e., 12-2222): " seg1 seg2
echo "Segment 1: $seg1"
echo "Segment 2: $seg2"
Теперь мы можем сохранить оба сегмента серийного номера в разных переменных:
Благодаря такому подходу, вы можете писать скрипты, обрабатывающие CSV файлы, не прибегая к внешним программам, таким как sed, awk, или Python. А здесь вы, как раз, можете посмотреть пример такого Bash скрипта, обрабатывающего CSV файлы.
Запрос истории Bash в терминале для повышения производительности.
Ранее мы обсуждали эффективное написание скриптов для оболочки Shell с помощью встроенных предопределенных параметров. Теперь давайте обсудим некоторые Bash нотации, позволяющие ускорить нашу работу с терминалом. Как вы уже знаете, Bash хранит историю выполненных команд в файле ~/.bash_history , и мы можем просматривать эту историю с помощью команды history
Существует несколько сокращений, для быстрого доступа записям истории Bash. Например вы можете использовать следующие нотации для доступа этим записям:
!-1 # Предыдущая команда. Иначе: !!
!-2 # Вторая команда в истории записей
!-3 # Третья команда в истории записей
Это не встроенные параметры — а специальный синтаксис, известный как расширение истории ( history expansion ). Вы можете настраивать файл истории с помощью HISTSIZE, HISTTIMEFORMAT .
Параметр $_ также может быть очень полезен — он позволяет получить аргументы командной строки предыдущей команды, так, что мы можем избежать долгого перепечатывания длинного набора аргументов выполненной команды:
touch long_file_name.sh
chmod +x $_