CodingStyle/Shell/TypicalCode
Типовые конструкции при программировании на Shell.
Оформление скрипта
В начале скрипта пишется конструкция
#!/bin/sh
если вы пишете на стандартном shell и
#!/bin/bash
если обязательны некоторые особенности bash.
Переменные
Определение локальных и глобальных переменных
Глобальные переменные указываем так:
MYBIGVAR=5
Локальные (в функциях):
local my_local_var="some var"
или так:
local my_local_var your_local_var my_local_var=5 your_local_var=$my_local_var
Пример:
myfunc() { local my_local_var if [ -n "$MYBIGVAR" ] ; then my_local_var="$MYBIGVAR" fi }
При последующей проверке нет разницы, была инициализирована переменная или нет.
Константы
Для уверенности, что присвоенное значение никто не изменит, нужно использовать модификатор readonly:
MYBIGVAR=5 ... readonly MYBIGVAR ... MYBIGVAR=new # error here
в этом примере мы сначала присвоили значение, а потом решили, что оно будет read-only. Можно присваивать и сразу:
readonly MYBIGVAR=5
Для локальных переменных записывается так:
readonly local my_local_var
Интересная особенность: глобальная read-only переменная не даёт определять и присваивать локальные переменные с таким же именем.
Варианты использования
Присваивание значений
Обратите внимание:
VAR=5 command
вызывает command, у которой среди переменных окружения будет VAR, равно 5.
VAR= 5 command
вызывает команду 5 с параметром command и пустым значением переменной VAR
VAR = 5 command
вызывает команду VAR с параметрами = 5 command
Пример:
func() { echo $VAR } VAR=10 VAR=5 func echo $VAR
вызывает функцию func, у которой среди переменных окружения будет VAR, равно 5. Выведет:
5 10
Занесение вывода команды в переменную
MYVAR="$(ls | head)"
Команды и функции
Определение функции
my_func() { echo "some function code" }
Все внешние команды ищутся по путям, записанным в переменную окружения PATH.
Вызов команды или функции
Обычно нет разницы, вызываем мы внешнюю команду или функцию.
myfunc 1 none somecommand params
Но стоит помнить, что вызвать функцию из других программ мы не можем:
find | xargs my_func
работать не будет, потому что тут внешняя команда xargs будет пытаться выполнить команду my_func.
Внутреннее использование допустимо и правильно:
find | my_filter
Возвращаемые из функции значения
Из функции можно вернуть только статус возврата 0..255.
Не делайте лишних проверок внутри функции.
func() { # 1. первый параметр должен быть не пустой, иначе выходим [ -n "$1" ] || return # 2. параметры совпадают, дальше не идём [ "$1" = "$2" ] && return # 3. если второй параметр пустой, вернёмся [ -z "$2" ] && return 2 # 4. выполняем действие superfunc "$2" "$1" }
Обратите внимание, что в конце функции не надо писать return $?, поскольку при любом выходе из функции она возвращает статус последней команды.
Коды возврата нашей func из примера:
- Если 1-й параметр пустой, сразу вернёт 1 (false)
- Если параметры совпадают, сразу вернём true
- Если 2-й параметр пустой, сразу вернём 2 (false)
- выполним функции superfunc и вернём статус её возврата, поскольку она последняя.
Использование параметров по умолчанию:
func() { local PARAM="$1" [ -n "$PARAM" ] || PARAM="default" echo "$PARAM" } func 10 func
Условия
Сравнение
Обычно для сравнения разных величин используется команда test. Она же «[». В зависимости от параметров test имеет статус возврата true или false:
func() { [ -n "$MYVAR" ] return }
func вернёт 1 (false), если MYVAR пустая.
Условие через if
Главное: if проверяет статус возврата (true или false), поэтому там может быть вписана любая команда.
if echo Hello | grep "e" ; then echo "There is «e» letter." fi
Пример:
if [ -z "$MYBIGVAR" ] ; then my_local_var=5 fi
echo "Very long command" && true if [ $? = 0 ] ; then echo Successful fi
Условие через && ||
Для соединения команд в любом месте можно использовать логические операции && и ||.
Например,
[ -n "$MYBIGVAR" ] || MYBIGVAR=5
(MYBIGVAR наполнена или присваиваем ей 5, то есть: проверяем, есть ли в переменной MYBIGVAR что-то непустое; если там пусто, присваиваем 5)
Таким образом записываются и сложные условия в if:
if [ -z "$MYZERO" ] && [ "$2" != "$MYVAR" ] ; then echo "1" elif [ "$MYZERO = "$MYVAR" ] ; then echo "2" else echo "default" fi
switch / case
Большие ветвления с выбором удобно делать через case:
case "$VAR" in a|b|c) echo "some char" ;; alpha|beta|gamma) echo "some char name" ;; a*) echo "some word started with a letter" ;; *) echo "something undetected: $VAR" ;; esac
Циклы
for
local i for i in Q W E ; do echo $i done выведет Q W E
local i for i in $(somecommand) ; do echo $i done
Файлы на a в каталоге test
local i for i in test/a* ; do echo $i done
while
while true; do echo "We are printing infinitely." sleep 1 done
while read
Чтение табличных данных из файла, например
$ cat input.data apple яблоко 5 berry ягода 4 #В конце последней строки должен быть перенос
while read engname rusname count ; do echo "$engname в переводе на русский: $rusname" done < input.data
Если нужно обработать с помощью while вывод команды, то вряд ли вас устроит вариант
print something | while read NN ; do MyVar=$NN done
Дело в том, что while из-за участия в pipe будет порождённым процессом, и изменённые им переменные не будут переданы в основной код.
Есть bash-специфичная конструкция <<<, которая исправит ситуацию:
while read NN ; do MyVar=$NN done <<<"$(print something)"
Работа с путями
Включение файлов
Для объединения кода, разбитого на несколько файлов, применяется вставка:
. /path/to/file
Файл просто вставляется в это место, как текст, поэтому скрипт с включением других скриптов стоит рассматривать как один большой файл.
Переход в место расположения скрипта
cd $(dirname "$0")
cd $(pwd)/$(dirname "$0")
Вычисления
a=10 echo $(($a+5))
Как не надо писать
[ $(echo | grep) ] command1 || command2 && command3
( потому что результат не очевиден (не ожидаем) )
func() { for i in A B C ; do echo "$i: $1" done } for i in alpha beta gamma ; do func $i echo $i done
( поскольку явно не указана локальность i в функции func, то произойдёт затирание значения i в основном цикле)