Bash. Основы программирования
Цикл статей-памяток по разработке полезных утилит на bash
По долгу профессии, мне, как программисту, необходимо автоматизировать свой рабочий процесс. Быстрые скриншоты с загрузкой на сервер, получение и обработка выделенного участка текста, парсинг данных, конвертация файлов и многое другое.. Источник в PDF.
Из этой статьи вы узнаете (или вспомните) азы, необходимые для разработки простых bash
сценариев.
Переменные
Переменные в bash
могут содержаться в 3х областях видимости:
- Локальные переменные; Простая переменная внутри конкретного сценария. Например, определение переменной, доступ к значению и удаление:
path="/home/user/stas/tmp.txt" echo -n $path # вывести значение переменной unset path # удалить переменную
Также, вы можете объявить локальную переменную внутри функции. Такая переменная будет доступна только внутри этой ф-ции:
local my_var;
- Переменные окружения (подробнее); Этот тип переменных доступен любой программе (сценарию), которая запущена из данной оболочки. Разместить переменную в окружении можно командой
export
:export var_name
- Переменные оболочки; Это переменные имеют имена порядкового номера (
$1
,$2
, ..) и содержат аргументы, переданные сценарию при запуске, например:./some_script.sh VAL1 VAL2 # внутри сценария
$0 имя скрипта $1 VAL1 $2 VAL2 $@ Содержит все аргументы командной строки $# Количество аргументов
Подстановка параметров и результата работы команд
Механизм подстановки команды (заключенная в апострофы или круглые скобки $(command)
команды вернет значение в переменную, завершающие символы новой строки удаляются):
current_date=`date +%Y-%m-%d`
# OR #
current_date=$(date +%Y-%m-%d)
echo $current_date # 2013-10-05
Получить результат значение переменной в другую переменную:
var=${arr[*]} # все элементы массива
Примечание
При подстановке параметра - фигурные скобки не обязательны, они служат для отделения имени переменной от соседних символов и строк.
Арифметика:
$(( арифметика ))
Раскрытие фигурных скобок
Раскрытие фигурных скобок - это механизм, с помощью которого можно генерировать строки произвольного вида. Генерация строки по шаблону (фигурные скобки могут быть вложенными):
echo sp{el,il,al}l #> spell spill spall
Условные операторы
Двойные квадратные скобки [[ ... ]] bash интерпретирует как один элемент с кодом возврата. Внутри разрешается использование операторов && и ||.
## Однострочная запись
[[ -f /dir/file ]] || { echo "File not exists"; exit 1; }
## Вложенные условия
if [ ... ] && [ ... ]; then
...
elif [[ ... && ... ]]; then
...
else
...
fi;
Пример проверки что переменная не пустая:
if [[ $some_var != '' ]]; then
echo 'Переменная не пустая'
fi
Комбинированные условия с отрицанием:
if [ -f /path/file.png ] && [ ! -z "$status" ]; then
echo "Файл и переменная существуют"
else
echo "Нет файла или переменной"
fi
Оператор case
:
case "$extension" in
(html|htm)
echo "Это HTML файл"
;;
pdf)
echo "Это PDF файл"
;;
*) echo "Не известный тип файла: $extension" ;;
esac
Условия сравнения
Смотрите доп. условия проверки файлов - https://www.tldp.org/LDP/abs/html/fto.html
### Файлы ###
-e Проверить что файл существует (-f, -d)
-f Файл существует (!-f - не существует)
-d Каталог существует
-s Файл существует и не пустой
-r Файл существует и доступен на чтение
-w ... на запись
-x ... на выполнение
-h Символическая ссылка
-b Файл существует и является блочным устройством
-с Файл существует и является символьным устройством (character device)
-p Файл существует и является потоковым устройством (pipe device)
-S Файл существует и является сокетом
### Строки ###
-z Пустая строка
-n Не пустая строка
== Равно (!= не равно)
### Числа ###
-eq Равно -ne Не равно
-lt Меньше -le Меньше или равно
-gt Больше -ge Больше или равно
Установка дефолтного значения переменной
Не работает для массивов!
Установка значения по умолчанию для аргумента:
SOME_VAR=${1:-'default'} # Если первый аргумент ($1) пустой - присвоить строку
SOME_VAR=${2:-$default} # Если аргумент $2 пустой - установить значение $default
: ${my_var:=$default} # Если $my_var пустая - присвоить ей значение из $default
Установка дефолтного значения для существующей переменной:
TEST='YES'
: ${TEST:='some string'}
echo $TEST # YES
Массивы и списки
IFS - разделитель элементов массива
При работе с массивами в качестве разделителя используется переменная IFS
. При работе с массивами вы можете задать собственный разделитель - просто установив значение переменной IFS
. Чтобы установить в качестве разделителя перенос каретки (окончание строки) задайте значение этой переменной: IFS=$'\n'
.
Создание и наполнение массивов
Инициализация (объявление):
files[0]=file.txt
files[1]=image.png
Разбить строку по словам и создать массив (join array with delimiter):
IFS=', ' read -r -a array <<< "one, two three"
arr=('hello world' other);
IFSorigin=$IFS
IFS=$'\n'
echo "${arr[*]}"
IFS=$IFSorigin
Создать массив из строки с указанием разделителя:
## Multiline content to array
IFSorigin=$IFS
IFS=$'\n'
read -a arr <<< "$content" ## для zsh используйте: arr=("${=content}")
IFS=$IFSorigin
echo ${arr[@]}
Добавить элементы в массив:
arr=("${arr[@]}" "New element 1" "New element 2")
Вывод элементов массива
Распечатать элементы массива ():
echo ${files[*]} # распечатать элементы массива не учитывает IFS
echo ${files[@]} # распечатать элементы массива с IFS в качестве разделителя
Количество элементов массива:
${#my_arr[@]}
Индекс случайного элемента массива:
rand_idx=$[RANDOM % ${#texts[@]}]
Вывести элементы массива через разделитель \n
:
printf -- '%s\n' "${arr[@]}" # выведет разделитель после последнего элемента
Срезать/получить первый символ первого элемента массива:
${arr:0:1}
Срезать первый элемент массива:
shift arr
Копировать массив (присвоить массив другой переменной):
new_arr=("${arr[@]}")
Проверить наличие элемента в массиве:
search_el='some'
arr=(some elements in array)
if [[ ${arr[(r)${search_el}]} == $search_el ]]; then
echo "Элемент ${search_el} найден"
fi
# if [[ ${arr[(r)some]} == some ]]; then ; echo yes; else; echo no; fi
Обход массивов, списков, строк
Примечание
Цикл работает в отдельном потоке, поэтому будьте внимательны при доступе к внешним переменным.
Обойти элементы массива:
arr=('one' 'two words')
for el in "${arr[@]}"; do
echo "$el"
done
Обход списка файлов в каталоге:
for f in $HOME/tmp/*; do
filename=$(basename "$f")
extension=${filename##*.}
if [ "$filename" == "stop.txt" ]; then
break
fi
if [ $extension != 'png' ]; then continue; fi
echo $f
done
Обход строк в многострочном тексте:
vendor/bin/propel database:reverse --help | while read line; do
echo $line
done
echo "one;two" | tr ";" "\n" | while read item; do
echo $item
done
Цикл while
(сценарий проверки интернет соединения):
while [ true ]; do
no_inet=`ping -q -c 1 ya.ru | grep "100% packet loss" | wc -l`
if [ $no_inet -eq 0 ]; then
echo "OK"
sleep 5
else
echo "Not connection"
notify-send "Пропал интернет" "Нет соединения с интернетом"
break
fi
done
Оператор select
(выбор пользователем значения из списка):
select i in Laravel Symfony2 Silex; do
case $i in
Laravel) echo "Выбран $i"; break; ;;
esac
done
Функции
Функции в bash несколько урезанные, однако могут принимать аргументы, возвращать вычисленное значение и, все-таки позволяют исключить дублирование кода в скриптах. Ниже костяк ф-ции:
some_sunction() {
# Объявляем переменную $str локальной и читаем в нее стандартный поток ввода
local str
read str
first_argument="$1"
second_argument="$2"
# ... тело функции
# Или читаем построчно входной поток
while read line; do
# Возвращаем список строк для последующей обработки
echo -n "${first_argument} and $second_argument"
done <<< file.txt
# Вернуть код завершения (0 - при успешном завершении)
# Код ответа доступен после выполнения ф-ции в переменной $?
return 0
}
Примечание
Если указать read несколько переменных,
то в первую попадёт первое слово; во вторую - второе слово; в последнюю - всё остальное. Читать поток можно только внутри { }
.
Функция вызывается как обычная команда:
echo 'content' | some_sunction arg1 arg2
some_var='второй аргумент'
result=$(some_sunction 'arg1' "$some_var")
# или так
result=`some_sunction`
return_code=$? # получить код возврата
Код возврата (завершения)
Каждая программа по завершению возвращает числовой код (статус) завершения в переменную окружения $?
(от 0 до 255). При успешном выполнении команда возвращает код 0, в случае ошибки - код будет целым числом. С помощью этой переменной вы можете проверять статус выполнения каждой необходимой программы или скрипта.
Примечание
Когда работа сценария завершается командой exit без параметров, то код возврата сценария определяется кодом возврата последней исполненной командой.
Потоки
Файл, из которого осуществляется чтение, называется стандартным потоком ввода, а в который осуществляется запись — стандартным потоком вывода.
Стандартные потоки:
0 stdin, ввод
1 stdout, вывод
2 stderr, поток ошибок
При перенаправлении потоков, вы можете указывать ссылки на определенные потоки. Например, перенаправим вывод и ошибки команды в файл:
command 2>&1 # ошибки (stderr) в stdout
command > ~/out.txt 2>&1 # stdout в файл
command &> ~/out.txt # весь вывод в файл
Перенаправление потоков
Для перенаправления потоков используются основные команды: <, >, >>, <<<, |. Рассмотрим как можно перенаправлять стандартные потоки.
Перенаправление потока вывода:
> перенаправить поток вывода в файл (файл будет создан или перезаписан)
>> дописать поток вывода в конец файла
Перенаправление потока ввода (прием данных):
< файл в поток ввода (файл будет источником данных)
<<< чтение ОДНОЙ строки вместо содержимого файла (для bash 3 и выше)
Перенаправление вывода ошибок:
2> перенаправить поток ошибок в файл
2>> дописать ошибки в файл (файл будет создан или перезаписан)
Примечание
Если вам нужно захватить вывод команды в переменную и при этом отобразить вывод на экране, используйте tee
:
my_var=$(my_script.sh | tee /dev/tty)
Подстановка процессов
Передать процессу команда1 файл (созданный налету канал или файл /dev/fd/...), в котором находятся данные, которые выводит команда2:
команда1 <(команда2)
Примеры
Логировать результат поиска и ошибки:
find . -maxdepth 1 -name '*.png' > ~/result.txt 2> ~/errors.txt
Эта конструкция позволяет читать из строки как из файла. Демонстрационный пример:
str='one,two';
tr ',' ' ' <<<$str
Создаем временный файл и записываем в него поток переданный скрипту:
tmp_file=$(tempfile) # /tmp/fileXXXXXX
cat > $tmp_file
А теперь откроем файл в текстовом редакторе с "отвязкой" (отключением) от терминала, подавляем вывод сообщений в терминал:
(sublime-text $tmp_file &) 2> /dev/null > /dev/null
Каналы
Стандартные потоки можно перенаправлять не только в файлы, но и на вход
других программ. Если поток вывода одной программы соединить с потоком
ввода другой программы, получится конструкция, называемая каналом,
конвейером или пайпом (от англ. pipe, труба).
Объединяем потоки команд в канал:
cmd1 | cmd2 | cmd3
Здесь стандартный поток вывода cmd1
подключен к входному потоку cmd2
, поток вывода cmd2
будет передан на вход cmd3
.
Конвееры
В составных командах и цепочке конвееров используются операторы управления ;, &&, ||. Тут все довольно просто, главное запомнить алгоритм и порядок, по которому работают эти операторы:
1. Команда cmd2
будет выполнена по завершении команды cmd1
не зависимо от результата работы cmd1
:
cmd1; cmd2
2. Команда cmd2
будет выполнена после успешного выполнения cmd1
(код завершения = 0):
cmd1 && cmd2
3. Команда cmd2
будет выполнена, если код завершения cmd1
отличен от 0 (произошла ошибка):
cmd1 || cmd2
#bash, #conditions, #pipe, #array