FreeBSD virtual environment management and repository

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