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