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 в основном цикле)