Bash  —  предустановленная командная оболочка в современных операционных системах на основе Unix. Ее используют в скриптах (сценариях) для автоматизации повторяющихся задач командной строки.

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

Новые версии Bash предоставляют ассоциативные массивы и такие связанные с производительностью функции, как передача по ссылке. Все это делает оболочку конкурентоспособной среди языков для написания скриптов.

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


Использование массивов в сценариях оболочки

Традиционная переменная в Bash обычно не имеет типа, но ее можно обрабатывать как целое, десятичное число или строку в соответствии с конкретным контекстом. Обычно такие переменные используют для хранения выходных данных команд, параметров алгоритма и других временных значений. Bash также поддерживает два типа массивов: одномерные (с числовым индексом) и ассоциативные (со структурой ключ-значение). Работать с массивами в Bash так же просто, как и в других популярных языках общего назначения с динамической типизацией, таких как Python, PHP и JavaScript.

Вот как создаются массивы в Bash:

#!/bin/bash

numbers=(2 4 5 2)

declare -a words
words[0]='Orange'
words[1]='Pineapple'

echo ${numbers[@]} ${words[@]}

Приведенный выше код выводит массив:

Работа с массивами в Bash

Проверить объявление каждой ссылки на массив можно с помощью встроенного declare:

Проверка объявлений массивов в Bash

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

#!/bin/bash

declare -a marks
marks+=(75 65 80 102 26) # метки класса A
marks+=(103 68) # метки класса B

# Удаление недопустимых меток
for i in "${!marks[@]}"; do
if ((marks[i] > 100)); then
unset "marks[$i]"
fi
done

# Сортировка всех меток
marks_s=($(printf '%s\n' "${marks[@]}" | sort -nr))

# Печать top-3
echo ${marks_s[0]} ${marks_s[1]} ${marks_s[2]}

Приведенный код порождает отдельный процесс для сортировки, поскольку использована внешняя команда sort. Но этого можно избежать, реализуя простой алгоритм сортировки, такой как selection sort, дополненный кодом Bash.


Создание карт и словарей

В некоторых сценариях командной оболочки приходится хранить данные пары «ключ-значение». Такие структуры данных программисты обычно используют при создании словарей, карт и контейнеров кэширования (посредством мемоизации). Встроенные структуры для хранения данных «ключ-значение» таких сценариев есть в Python. А как использовать структуру словаря в Bash?

Функция ассоциативного массива для хранения данных ключ-значение появилась в Bash с версии 4.0. Вот простой пример ассоциативных массивов Bash:

#!/bin/bash

declare -A marks=([john]=75 [doe]=82 [ann]=83 [ava]=72)
for key in "${!marks[@]}"; do
printf "$key \t ${marks[$key]} \n"
done

Здесь использован синтаксис !mapvar[@] для извлечения всех ключей словаря в виде массива для итерации. Приведенный выше код печатает все ключи и значения:

Работа с ассоциативными массивами Bash

Для манипуляции данными ассоциативного массива и доступа к ним в Bash имеется минимальный синтаксис. Работа с ассоциативными массивами Bash аналогична работе со словарями Python. Вот пример:

#!/bin/bash

read -p "Enter coords (i.e., [x]=10 [y]=12): " coords
declare -A "coords=($coords)"

if [ ! -v "coords[x]" ]; then
coords[x]=5
fi

if [ ! -v "coords[y]" ]; then
coords[y]=10
fi

for key in "${!coords[@]}"; do
printf "$key = ${coords[$key]} \n"
done

Приведенный исходный код запрашивает у пользователя координаты x и y, устанавливает значения по умолчанию для отсутствующих значений осей и печатает их на терминале. Здесь использован синтаксис ! -v, аналогичный not in, обычно используемому со словарями Python.


Реализация поддержки именованных аргументов

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

Предоставляя аргументы командной строки для других команд/процессов, вы также можете передать их в сценарий Bash. Предположим, что нужно передать в скрипт два целочисленных значения. Затем можно легко использовать $1 и $2 для доступа к значениям первого и второго аргумента соответственно. Но все усложняется, когда используется больше индексированных аргументов и нужно реализовать необязательные аргументы (также известные как флаги или параметры командной строки). 

В качестве решения для этого сценария можно использовать именованные аргументы со встроенной функцией getopts. С помощью следующего сценария оболочки можно переопределить некоторые значения в сценарии по умолчанию:

#!/bin/bash

title="Information"
message="Hello world"

while getopts ":t:m:" option; do
echo $option
case "${option}" in
t)
title=${OPTARG}
;;
m)
message=${OPTARG}
;;
esac
done

zenity --info --title="$title" --text="$message"

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

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

Встроенная функция getopts поддерживает использование только однобуквенных опций. Можно использовать getopt для использования длинных опций (например,  — title), как показано в этом Gist.


Использование в функциях передачи по ссылке

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

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

#!/bin/bash

function change_str_var() {
local str_new="Bash"
local -n str_ref=$1
echo "$str_ref -> $str_new" # Python -> Bash
str_ref=$str_new
}

str="Python"
change_str_var str
echo $str # Bash

Приведенная выше функция change_str_var создает локальную ссылку str_ref на глобальную строку str с помощью команды local. Затем она присваивает новое строковое значение, переопределяя старое значение.

Некоторые программисты используют внутри функции команду echo и вызывают конкретную функцию с помощью подстановки команд, чтобы вернуть значение из функции Bash (поскольку нативное ключевое слово return в Bash поддерживает только возврат допустимого выходного кода). Это порождает еще одну дочернюю оболочку с потреблением дополнительных ресурсов. Таким образом, используя передачу по ссылке, в новой версии Bash можно писать более производительные возвраты из функции.


Использование атрибутов типа и модификатора для переменных

Как известно, Bash  —  нетипизированный командный язык. Другими словами, переменные данные он обычно обрабатывает как строки, но в соответствии с контекстом (т. е. арифметическими расширениями). С другой стороны, Bash также позволяет использовать атрибуты типов и предоставляет два встроенных типа массивов. Даже с этими особенностями невозможно идентифицировать Bash как чисто динамически типизированный язык. Но эти переменные атрибуты помещают Bash где-то посередине между нетипизированным и динамически типизированным языками.

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

Добавление целочисленного атрибута к переменной Bash

Bash также позволяет создавать константы с помощью команды declare -r. Когда сценарий пытается изменить константу, Bash выводит на экран сообщение об ошибке. Более того, можно создавать массивы с помощью встроенной команды declare, как уже отмечалось выше.

Bash также позволяет добавлять к переменным некоторые атрибуты-модификаторы. Например, следующим образом можно создать строки, содержащие только строчные или только прописные буквы:

#!/bin/bash

declare -l lc_str="Hello World"
declare -u uc_str
uc_str="Hello"
uc_str="World"

echo $lc_str # hello world
echo $uc_str # WORLD

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

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Shalitha Suranga: 5 Bash Coding Techniques That Every Programmer Should Know

Предыдущая статья5 ведущих веб-фреймворков 2023 года
Следующая статьяКак создать локальное средство генерации кода с open source моделями и библиотекой Guidance от Microsoft