2020-10 upd: we reached the first fundraising goal and rented a server in Hetzner for development! Thank you for donating !
Shell scripting best practices
Данный текст — нарост из опыта sh-скриптования, некий style которым я руководствуюсь при написании скриптов на sh на *nix платформах. Часть из них — лишь мои персональные предпочтения.
Временные файлы
* Если скрипту необходимо хранить временные данные в промежуточных файлах, используйте mktemp(1) для автоматического создания уникальный временных файлов в /tmp каталоге, гарантирующую уникальность, или спользуйте $$ (pid) в названии временного файла.
Post-action, подчищаем за собой по выходу из скрипта
* Если скрипт создает временные файлы, по завершению (в том числе аварийному) он должен за собой все подчищать (или выполнить какие-то другие post-функции). Для этого пользуемся фунционалом trap, не забывая перечислить возможные сигналы, по которым скрипт может завершиться.
Для удобства, пишем не номера сигналов, а их макросы, например:
trap "rm -rf /tmp/myprogdir; rm -f /tmp/mytmp" HUP INT ABRT BUS TERM EXIT
Если по ходу выполнения скрипта вы хотите дописывать trap, удобно это делать через отдельную переменную, например:
#!/bin/sh
TRAP="echo Bye"
trap "${TRAP}" EXIT
do_something()
trap "${TRAP}; echo Cul8r" EXIT
do_something_else()
Документируем
* в начале функций пишем краткое описание функции, какие параметры и в качестве чего она принимает и что получается на выходе.
..
#algebral sum for $1 and $2, return $res as result
function sum()
{
..
Объявляем локальные переменные функции
* Все переменные, которые нужны только функции, описываем как локальные (также, это защита от того, что вы переназначите уже где-то ранее объявленную и где-то позже используемую переменную, например TERM, LANG, IFS и тд.
Также, локальные переменные удобно условиться писать с подчеркивания, а переменные окружения — в верхнем регистре.
Например:
#algebral sum for $1 and $2, return $res as result
function sum() {
local _a _b
[ -z "${1}" -o -z "${2}" ] && return 1
_a=$1
_b=$2
res=$(( _a + _b ))
}
sum 2 5
echo ${res}
Предпочитайте build-in операции внешним утилитам.
При математических операциях, пользуемся встроенной в sh математикой, а не внешними утилитами как *bc* или *expr*.
Например, вместо `expr $a=$a+1`, делаем:
a=$(( a + 1 ))
b=$(( a / 2 ))
Для парсинга par=val используйте %% и ##, например
a="hostname-value"
Вместо:
p1=`echo $a |tr -d "-" |awk '{printf $1}'`
p2=`echo $a |tr -d "-" |awk '{printf $1}'`
Делаем:
p1=${a%%-*}
p2=${a##*-}
или
p1=${a%-*}
p2=${a#*-}
пример, когда необходимо распарить из файла:
while read line; do
line=${line%%=*}
done < /path/to/file
Логические И-ИЛИ вместо if-then блоков, continue/return/break
Стараемся избегать большого количества if; then; fi конструкций, там где это возможно, заменяем их логичискими *И — &&* или *ИЛИ — ||*.
Например:
Конструкции вида:
if [ $a -gt 10 ]; then
if [ $b -ne 10 ]; then
if [ $z "${t}" ]; then
...
fi
exit //выйдем если b не равно 10
fi
exit //выйдем, если a меньше 10
fi
Стаемся не раздувать и пишем так:
[ $a -ne 10 ] && exit
[ $b -gt 10 ] && exit
...
(читаем как: если условие правильно, то выполним то что после && )
либо
[ $a -gt 10 ] || exit
[ $b -ne 10 ] || exit
(читаем как: если условие правильно, идем дальше, иначе, выполним то что после || )
Простые конструкции без *else/elif* с одной командой внутри, заменяем на &&
Вместо:
if [ -n "$filled_par" ]; then
echo "ok"
fi
Пишем:
[ -n "$filled_par" ] && echo "ok"
Если внутри конструкции идет уже больше двух++ команд, то вместо && || наоборот ухудшают читаемость и в данном случае лучше использовать *if / fi* блоки:
Вместо:
[ -n "$filled_par" ] && echo "ok" && date && echo "Cool"
или
[ -n "$filled_par" ] && {
echo "ok"
date
echo "Cool"
}
пишем:
if [ -n "$filled_par" ]; then
echo "ok"
date
echo "Cool"
fi
Если внутри цикла идет проверка переменной, не раздувайте код на if / else, используйте оператор continue:
Вместо
for i in PARAM; do
if [ "${i}" = "name" ]; then
do something
and something else
and something else
and something else
and something else
and something else
and something else
and many-many other action here
else
nop ;(
fi
done
Пишите:
for i in PARAM; do
[ "${i}" != "name" ] && continue
do something
and something else
and something else
and something else
and something else
and something else
and something else
and many-many other action here
done
Аналогично с функциями и оператором return:
Вместо:
# argument $1 is mandatory
# show one hundred $1 here
function checkit()
{
local _i
if [ -n "${1}" ]; then
for _i in `jot 0 1000`; do
echo ${1}
done
else
echo "argument is mandatory"
fi
}
Пишите:
# argument $1 is mandatory
# show one hundred $1 here
function checkit()
{
local _i
[ -z "${1}" ] && echo "argument is mandatory" && return 1
for _i in `jot 0 1000`; do
echo ${1}
done
}
Stderr и функция err()
Ошибки необходимо выводить в stderr: echo "Error" >&2. Удобно вывод ошибок обернуть в отдельную процедуру err и использовать ее через && :
# fatal error. Print message then quit with exitval
err()
{
exitval=$1
shift
echo -e "$*" 1>&2
exit $exitval
}
[ -z "${must_be_not_empty}" ] && err 1 "param must_be_not_empty is empty"
Скобки процедур
Скобкам начала процедур и конца отдаем перенос строчки. Например:
плохо читаемо:
err() {
echo "hi" }
намного лучше:
err()
{
echo "hi"
}
Закрытие if и отступы
При условии if/then, закрывающая fi должна всегда находится на уровне соответствующего if, например:
плохо читаемо:
if true;
then
echo "lol"
fi
намного лучше:
if true; then
echo "lol"
fi
Cases и отступы
Конец case (*;;*) должен находиться на уровне конца сценария этого кейса.
Например:
плохо читаемо:
case $value in
1) do_something ;;
2) if [ 1 -eq 1 ]; then
echo "Great!"
fi
;;
esac
намного лучше:
case $value in
1) do_something
;;
2) if [ 1 -eq 1 ]; then
echo "Great!"
fi
;;
esac
Экранируйте переменные и указывайте границы переменных
Всегда указывайте границы переменной через { и }
Вместо:
echo $value
Пишите:
echo ${value}
Переменные со строковым содержим, экранируйте кавычками
Вместо:
if [ -f ${myfile} ]; then
..
Пишите:
if [ -f "${myfile}" ]; then
..
Избегайте лишних телодвижений
Каждый вызов утилит — это отдельная и порой ненужная работа для ОС
Вместо:
cat file | grep something
Пишите:
grep somethins file
Вместо:
cat file | command
Пишите:
command < file
Используйте heredoc там, где собираетесь использовтаь большое количество echo
Вместо:
echo "Hello"
echo "World"
echo "My name is ${name}"
Пишите:
cat << EOF
Hello
World
My name is ${name}
EOF
Избегайте лишних cat, grep там, где позволяет справится с обработкой функционал одной утилиты:
Вместо:
cat file |grep test |awk '{printf $2}'
Пишите:
awk '/test/{print $2}' file
Используйте IFS для установки разделителя
Вместо:
#!/bin/sh
myvalue="one|two|three"
for i in `echo $myvalue|tr "|" " "`; do
echo $i
done
Используйте:
#!/bin/sh
myvalue="one|two|three"
IFS="|"
for i in $myvalue; do
echo ${i}
done
Используйте возможность sh немедленно завершить выполнение скрипта при критических ошибках
Устанавливайте флаг немедленного завершения при возвращаемых кодах ошибок != 0
set +o errexit
set -o errexit
снимая и устанавливая принудительное завершение скрипта на тех участках кода, ошибки в которых фатальны для дальнейшего выполнения.
Используйте true для тех команд, которые не фатальны но идут в участке, который помечен завершаемый при любой ошибке.
Например:
#!/bin/sh
set -e # no all error is critical
mkdir /tmp/ole.$$
# this is produce error, couse -r needed for remove dir
rm -f /tmp/ole.$$ ||true
rmdir /tmp/ole.$$