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.$$