[Перевод] Google's Shell Style Guide (на русском)
Какой Shell использовать
Bash
единственный язык shell скриптов, который разрешается использовать для исполняемых файлов.
Скрипты должны начинаться с #!/bin/bash
с минимальным набором флагов. Используйте set
для установки опций shell, что бы вызов вашего скрипта как bash
не нарушило его функциональности.
Ограничение всех shell скриптов до bash, дает нам согласованный shell язык, который установлен на всех наших машинах.
Единственное исключение составляет если вы ограничены условиями того под что вы программируете. Одним из примеров могут стать пакеты Solaris SVR4, для которых требуется использование обычного Bourne shell для любых скриптов.
Когда использовать Shell
Shell следует использовать только для небольших утилит или простых скрптов-оберток.
Хотя shell-скриптинг не является языком разработки, он используется для написания различных утилит во всем Google. Это руководство по стилю является скорее признанием его использования, а не предложением использовать его в широком применении.
Некоторые рекомендации:
- Если вы чаще всего вызываете другие утилиты и делаете относительно небольшое манипулирование данными, shell является приемлемым выбором для задачи.
- Если производительность имеет значение, используйте что-нибудь другое, но не shell.
- Если вы обнаружите, что вам нужно использовать массивы более чем для назначения
${PIPESTATUS}
, вы должны использовать Python. - Если вы пишете скрипт длиной более 100 строк, вы, вероятно, должны писать его на Python. Имейте в виду, что скрипты растут. Перепишите свой скрипт на другом языке раньше, чтобы избежать трудоемкой перезаписи позднее.
Расширения файлов
Исполняемые файлы не должны иметь расширения (сильно предпочтительно) или расширение .sh
. Библиотеки должны иметь расширение .sh
и не должны быть исполняемыми.
Нет необходимости знать, на каком языке написана программа при ее выполнении, а shell не требует расширения, поэтому мы предпочитаем не использовать его для исполняемых файлов.
Однако для библиотек важно знать, на каком языке она написана, и иногда бывает необходимо иметь похожие библиотеки на разных языках. Это позволяет иметь идентично названные файлы библиотек с идентичными целями, но наприсанные разных языках должны быть идентично названы, за исключением суффикса, специфичного для языка.
SUID/SGID
SUID и SGID запрещены на shell-скриптах.
Тут слишком много проблем с безопасностью, из-за чего почти невозможно обеспечить достаточную защиту SUID/SGID. Хотя bash усложняет запуск SUID, это все еше возможно на некоторых платформах, поэтому мы явно запрещаем его использование.
Используйте sudo
для обеспечения повышенного доступа, если вам это необходимо.
STDOUT vs STDERR
Все сообщения об ошибках должны отправляться в STDERR
.
Это помогает разделить нормальное состояние от актуальных проблем.
Функцию для вывода сообщений об ошибках рекомендуется использовать вместе с другой информацией о состоянии.
err() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2
}
if ! do_something; then
err "Unable to do_something"
exit "${E_DID_NOTHING}"
fi
Заголовок файла
Начинайте каждый файл с описанием его содержимого.
Каждый файл должен иметь заголовок из комментария, включающий краткое описание его содержимого. Уведомление об авторских правах и информация об авторе являются необязательными.
Пример:
#!/bin/bash
#
# Perform hot backups of Oracle databases.
Комментарии к функциям
Любая функция, которая не является очевидной и короткой, должна быть прокомментирована. Любая функция в библиотеке должна быть прокомментирована независимо от ее длины или сложности.
Нужно сделать так, чтобы кто-нибудь другой понял как использовать вашу программу или как использовать функцию в вашей библиотеке, просто прочитав комментарии (и необходимости самосовершенствования), не читая код.
Все комментарии к функциям должны включать:
- Описание функции
- Используемые и измененные глобальные переменные
- Получаемые аргументы
- Возвращаемые значения, отличные от стандартных exit codes в последней команде.
Пример:
#!/bin/bash
#
# Perform hot backups of Oracle databases.
export PATH='/usr/xpg4/bin:/usr/bin:/opt/csw/bin:/opt/goog/bin'
########################################
# Cleanup files from the backup dir
# Globals:
# BACKUP_DIR
# ORACLE_SID
# Arguments:
# None
# Returns:
# None
########################################
cleanup() {
...
}
Комментарии по реализации
Комментируйте сложные, неочевидные, интересные или важные части вашего кода.
Это предполагается обычной практикой комментирования кода в Google. Не комментируйте все. Если есть сложный алгоритм или вы делаете что-то необычное, добавьте короткий комментарий.
TODO Комментарии
Используйте TODO комментарии для кода, который является временным, краткосрочным решением или довольно хорошим, но не идеальным.
Это соответствует соглашению в руководстве C++.
TODO коментарии должны включать слово TODO заглавными буквами, а затем ваше имя в круглых скобках. Двоеточие является необязательным. Предпочтительно также указывать номер бага/тикета рядом с элементом TODO.
Пример:
# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
Хотя вы должны следовать стилю, который уже используется в изменяемых вами файлах, для любого нового кода требуется следующее.
Отступы
Отступ 2 пробела. Без табов.
Используйте пустые строки между блоками, чтобы улучшить читаемость. Отступ — это два пробела. Независимо от того что вы делаете, не используйте табы. Для существующих файлов оставайтесь верными текущим отступам.
Длина строк и длина значений
Максимальная длина линии — 80 символов.
Если у вас есть необходимость в написании строк длиной более 80 символов, это должно быть сделано с помощью here document
или, если это возможно, встроенным newline
. Литеральные значения, которые могут быть длиннее чем 80 символов и не могут быть разделены разумно разрешены, но настоятельно рекомендуется найти способ сделать их короче.
# Используйте 'here document's
cat <
Пайплайны
Пайплайны должны быть разделены каждый на одну строку, если они не помещаются на одной строке.
Если папйплайн помещается в одну строку, он должен быть на одной строке.
Если нет, его следует разделить, что бы каждая секция находился на новой строке и отступом на 2 пробела для следующей секции. Это относится к цепочке команд, объединенной с использованием '|', а также к логическим соединениям, использующим '||' и '&&'.
# Все помещается на одной линии
command1 | command2
# Длинные комманды
command1 \
| command2 \
| command3 \
| command4
Циклы
Помещайте ; do
и ; then
на тойже линии что и while
, for
или if
.
Циклы в оболочке немного разные, но мы следуем тем же принципам как и с фигурными скобками при объявлении функций. То есть: ; then
и ; do
должны быть в той же строке, что и if
/for
/while
. else
должен быть в отдельной строке, а закрывающие операторы должны быть на собственной линии, вертикально выровненной с открывающей инструкцией.
Пример:
for dir in ${dirs_to_cleanup}; do
if [[ -d "${dir}/${ORACLE_SID}" ]]; then
log_date "Cleaning up old files in ${dir}/${ORACLE_SID}"
rm "${dir}/${ORACLE_SID}/"*
if [[ "$?" -ne 0 ]]; then
error_message
fi
else
mkdir -p "${dir}/${ORACLE_SID}"
if [[ "$?" -ne 0 ]]; then
error_message
fi
fi
done
Оператор case
- Отделяйте варианты в 2 пробела.
- Для однострочных вариантов требуется пробел после закрывающей скобки шаблона и перед
;;
. - Длинные или многокомандная варианты должны быть разделены на несколько строк с шаблоном, действиями и
;;
на раздельные строки.
Соответствующие выражения отступают на один уровень от case
и esac
. Многострочные действия так же имеют отступы на отдельный уровнь. Нет необходимости помещать выражения в кавычки. Шаблонам выражений не должны предшествовать открытые круглые скобки. Избегайте использование &;
и ;;&
обозначений.
case "${expression}" in
a)
variable="..."
some_command "${variable}" "${other_expr}" ...
;;
absolute)
actions="relative"
another_command "${actions}" "${other_expr}" ...
;;
*)
error "Unexpected expression '${expression}'"
;;
esac
Простые команды могут быть помещены в одну строку с шаблоном и ;;
пока выражение остается читаемым. Это часто подходит для обработки однобуквенных опций. Когда действия не помещаются в одну строку, оставьте шаблон в своей строке, следующей действия, затем ;;
также в собственной линии. Когда это та же строка, что и с действими, используйте пробел после закрывающей скобки шаблона и другой перед ;;
.
verbose='false'
aflag=''
bflag=''
files=''
while getopts 'abf:v' flag; do
case "${flag}" in
a) aflag='true' ;;
b) bflag='true' ;;
f) files="${OPTARG}" ;;
v) verbose='true' ;;
*) error "Unexpected option ${flag}" ;;
esac
done
Расширение переменных
В порядке приоритета: соблюдайте то, что уже используется; добавляйте свои переменные в кавычки; предпочитайте »${var}» перед »$var», но уточняйте детали.
Это скорее рекомендации, поскольку тема достаточно противоречива для обязательного регулирования. Они перечислены в порядке приоритета.
- Импользуйте тот же стиль, что вы найдете в существующем коде.
- Помещайте переменные в кавычки, смотрите раздел Кавычки ниже.
-
Не помещайте в кавычки единичные символы специфичные для shell / позиционные параметры, если это строго не необходимо и во избежании глубокой путаницы.
Предпочитайте фигурные скобки для всех остальных переменных.# Раздел рекомендуемых случаев # Придерживайтесь стиля для 'специальных' переменных: echo "Positional: $1" "$5" "$3" echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..." # Фигурные скобки обязательны: echo "many parameters: ${10}" # Фигурные скобки исключающие путаницу: # Output is "a0b0c0" set -- a b c echo "${1}0${2}0${3}0" # Предпочтительный стиль для остальных переменных: echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}" while read f; do echo "file=${f}" done < <(ls -l /tmp) # Раздел нежелательных случаев # Переменные без кавычек, переменные без фигурных скобок, # единичные символы в фигурных скобках, специфичные для shell echo a=$avar "b=$bvar" "PID=${$}" "${1}" # Неправильное использование: # должно расскрываться как "${1}0${2}0${3}0", а не "${10}${20}${30} set -- a b c echo "$10$20$30"
- Всегда используйте кавычки для значений, содержащие переменные, подстановки команд, пробелы или метасимволы оболочки, до тех пор пока не требуется безопасное расскрытие значений не в кавычках.
- Предпочитайте кавычки для значений которые являются «словами» (в отличие от параметров команд или имен путей)
- Никогда не помещайте в кавычки целые числа.
- Знайте как работают кавычки для шаблонов совпадений в
[[
. - Используйте
"$@"
, если у вас особых причин использовать$*
.
# 'Одинарные' кавычки указывают, что никакой подстановки не требуется.
# "Двойные" кавычки указывают, что полстановка необходима/допускается.
# Простые примеры
# "подстановка комманды в кавычках"
flag="$(some_command and its args "$@" 'quoted separately')"
# "переменные в кавычах"
echo "${flag}"
# "никогда помещайте целые числа в кавычки"
value=32
# "помещайте подстановку комманд в кавычки", даже если вы ожидаете числа
number="$(generate_number)"
# "Используйте кавычки для слов", но не обязательно
readonly USE_INTEGER='true'
# "Используйте кавычки для специальных мета-символов shell"
echo 'Hello stranger, and well met. Earn lots of $$$'
echo "Process $$: Done making \$\$\$."
# "опции комманд и имена путей"
# (Здесь предполагается, что $1 содержит значение)
grep -li Hugo /dev/null "$1"
# Менее простые примеры
# "Используйте кавычки для переменных, если не доказанно что": ccs не может быть пустым
git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}
# Предикат позиционного параметра: $1 модет быть удален
# Одинарные кавычки оставляют регулярное выражение как есть.
grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"}
# Для передачи аргументов,
# "$@" правильно почти всегда, и
# $* неправильно почти всегда
#
# * $* и $@ будут разделены пробелами, разбивая аргументы
# которые содердат пропуски пропуская пустые значения;
# * "$@" будет передавать аргументы как есть, так что
# никакие из переданныз аргументов не будут потеряны;
# В большинстве случаев это то, что вы и хотите получить
# передавая аргументы
# * "$*" расскрывается в один аргумент, соединяя остальные аргументы
# в один разделяя их (обычно) пробелами,
# Так что отсутсвие аргументов передаст пустую строку
# (Почитайте 'man bash' для nit-grits ;-)
set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$*"; echo "$#, $@")
set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$@"; echo "$#, $@")
Подстановка комманд
Используйте $(command)
вместо обратных кавычек.
Вложенные обратные кавычки требуют экранирования внутренних с помощью \
. Формат $ (command)
не изменяется в зависимости от вложенности и его легче читать.
Пример:
# Это предпочтительнее:
var="$(command "$(command1)")"
# Это нет:
var="`command \`command1\``"
Проверки, [
и [[
[[ ... ]]
более предпочтительнее чем [
, test
или /usr/bin/[
.
[[ ... ]]
уменьшает возможность ошибки, поскольку не происходит разрешение пути или разделение слов между [[
и ]]
, и [[ ... ]]
позволяет использовать регулярное выражение, где [ ... ]
нет.
# Это гарантирует, что строка слева состоит из символов
# типа `alnum`, за которым следует имя строки.
# Обратите внимание, что правая сторона не должена быть
# в кавычках. Для подробностей, смотрите
# E14 по адресу https://tiswww.case.edu/php/chet/bash/FAQ
if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
echo "Match"
fi
# Это соответствует точному шаблону "f*" (в данном случае не сработает)
if [[ "filename" == "f*" ]]; then
echo "Match"
fi
# Это выдаст ошибку "too many arguments", так как f* будет расскрыт
# в содержимое текущей директории
if [ "filename" == f* ]; then
echo "Match"
fi
Проверка значений
Используйте кавычки, а не дополнительные символы, где это возможно.
Bash достаточно умен, чтобы работать с пустой строкой в тесте. Поэтому, полученный код намного проще читать, используйте проверки для пустых/непустых значений или пустых значений, без использования дополнительных символов.
# Делайте так:
if [[ "${my_var}" = "some_string" ]]; then
do_something
fi
# -z (длина строки равна нулю), и -n (длина строки не равна нулю):
# предпочтительнее для проверки пустого значения
if [[ -z "${my_var}" ]]; then
do_something
fi
# Это допустимо (пустые кавычки), но не рекомендуется:
if [[ "${my_var}" = "" ]]; then
do_something
fi
# Но не так:
if [[ "${my_var}X" = "some_stringX" ]]; then
do_something
fi
Чтобы избежать путаницы в том, что вы проверяеете, явно используйте -z
или -n
.
# Используйте это
if [[ -n "${my_var}" ]]; then
do_something
fi
# Вместо этого, поскольку ошибки могут возникать, если ${my_var}
# расскроется в флаг для проверки.
if [[ "${my_var}" ]]; then
do_something
fi
Выражения подстановки для имен файлов
Используйте явный путь при создании выражений подстановки для имен файлов.
Поскольку имена файлов могут начинаться с символа -
, гораздо безопаснее расскрывать выражение подстановки как ./*
вместо *
.
# Вот содержимое каталога:
# -f -r somedir somefile
# Это удаляет почти все в директории с force
psa@bilby$ rm -v *
removed directory: `somedir'
removed `somefile'
# В отличие от:
psa@bilby$ rm -v ./*
removed `./-f'
removed `./-r'
rm: cannot remove `./somedir': Is a directory
removed `./somefile'
Eval
eval
следует избегать.
Eval позволяет раскрыть переменные передаваемые в вводе, но он так же может установить и другие переменные, без возможности их проверки.
# Что тут установленно?
# Успешно ли завершилось? Частично или полностью?
eval $(set_my_variables)
# Что произойдет, если одно из возвращаемых значений имеет в нем пробел?
variable="$(eval some_function)"
Пайпы в While
Используйте подстановку команд или цикл for
, предпочтительнее пайпов в while
. Переменные, измененные в цикле while
, не распространяются на родителя, потому что команды цикла выполняются в сабшелле.
Неявный сабшелл в пайпе в while
может затруднить отслеживание ошибок.
last_line='NULL' your_command | while read line; do last_line="${line}" done # Это вернет 'NULL' echo "${last_line}"
Используйте цикл for, если вы уверены, что ввод не будет содержать пробелы или специальные символы (обычно это не предполагает пользовательский ввод).
total=0
# Делайте так, только если в возвращаемых значениях отсутствуют пробелы.
for value in $(command); do
total+="${value}"
done
Использование подстановки комманды позволяет перенаправить вывод, но выполняет команды в явном сабшеле, в отличии неявного сабшела, который создает bash для цикла while
.
total=0
last_file=
while read count filename; do
total+="${count}"
last_file="${filename}"
done < <(your_command | uniq -c)
# Это выведет второе поле последней строки вывода из
# комманды.
echo "Total = ${total}"
echo "Last one = ${last_file}"
Используйте циклы while
, где нет необходимости передавать сложные результаты в родительский shell — это типично, когда требуется более сложный «парсинг». Помните, что простые примеры, порой, гораздо проще решить с использованием такого инструмента, как awk. Это также может быть полезно, когда вы специально не хотите изменять переменные родительской среды.
# Тривиальная реализация выражения awk:
# awk '$3 == "nfs" { print $2 " maps to " $1 }' /proc/mounts
cat /proc/mounts | while read src dest type opts rest; do
if [[ ${type} == "nfs" ]]; then
echo "NFS ${dest} maps to ${src}"
fi
done
Названия функций
В нижнем регистре, с подчеркиваниями для разделения слов. Разделите библиотек с ::
. После имени функции требуются скобки. Ключевые слова функций необязательно должны использоваться последовательно во всем проекте.
Если вы пишете отдельные функции, используйте строчные и отдельные слова с подчеркиванием. Если вы пишете пакет, разделяйте имена пакетов с помощью ::
. Скобки должны быть в той же строке что и имя функции (как и в других языках в Google), и не иметь пробел между именем функции и скобкой.
# Одиночная функция
my_func() {
...
}
# Часть пакета
mypackage::my_func() {
...
}
Ключевое слово функции неотделенно от ()
, когда оно присутствует после имени функции, это улучшает их быструю идентификацию как функций.
Название переменных
Что касается имен функций.
Имена переменных для циклов должны быть одинаково названы для любой переменной, которую вы перебираете.
for zone in ${zones}; do
something_with "${zone}"
done
Названия константы переменных окружения
Все заглавными буквами, разделенны символами подчеркивания, объявлены в верхней части файла.
Константы и всё, что экспортируется в окружение, должны быть в верхнем регистре.
# Константа
readonly PATH_TO_FILES='/some/path'
# Константа, и переменная
declare -xr ORACLE_SID='PROD'
Некоторые вещи остаются постоянными при их первой установке (например, через getopts
). Таким образом, это вполне нормально устанавливать константу через getopts
или на основе условия, но она должна быть сделана readonly
сразу после этого. Обратите внимание, что declare
не работает с глобальными переменными внутри функций, поэтому рекомендуется readonly
или export
вместо этого.
VERBOSE='false'
while getopts 'v' flag; do
case "${flag}" in
v) VERBOSE='true' ;;
esac
done
readonly VERBOSE
Назания исходных файлов
Нижний регистр, с подчеркиванием для разделения слов, если это необходимо.
Это касается соответствия другим стилям кода в Google: maketemplate
или make_template
, но не make-template
.
Переменные только для чтения
Используйте readonly
или declare -r
, чтобы убедиться, что они только для чтения.
Поскольку глобальные широко используются в shell, важно уловить ошибки при работе с ними. Когда вы объявляете переменную, предназначенную только для чтения, сделайте это явным.
zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
if [[ -z "${zip_version}" ]]; then
error_message
else
readonly zip_version
fi
Использование локальных переменных
Объявляйте переменные, зависящие от функции, с local
. Объявление и назначение должны выполняться на разных строках.
Убедитесь, что локальные переменные видны только внутри функции и ее дочерних элементов, используя local
при их объявлении. Это позволяет избежать загрязнения глобального пространства имен и непреднамеренно устанавливать переменные, которые могут иметь значение вне функции.
Объявление и присвоение должны идти разными командами, когда значение присваивания обеспечивается подстановкой команды; поскольку local
не обрабатывает exit code из подстановленной команды.
my_func2() {
local name="$1"
# Разделяйте строки для декларации и назначения:
local my_var
my_var="$(my_func)" || return
# НЕ ДЕЛАЙТЕ этого: $? содержит exit code от 'local', а не my_func
local my_var="$(my_func)"
[[ $? -eq 0 ]] || return
...
}
Расположение функций
Поместите все функции в файл чуть ниже констант. Не скрывайте исполняемый код между функциями.
Если у вас есть функции, поместите их все вместе в верхней части файла. Только включения, команды set и константы настройки, могут быть выполнены до объявления функций.
Не скрывайте исполняемый код между функциями. Это делает код трудным для подражания и приводит к неприятным неожиданностям при отладке.
main
Функция, называемая main
, требуется для сценариев достаточно длинных, чтобы содержать хотя бы одну другую функцию.
Чтобы можно было легко найти начало программы, поместите основную программу в функцию main
в самом низу функций. Это обеспечивает консистентность с остальной базой кода, а также позволит определить больше переменных как локальных (что невозможно сделать, если основной код не является функцией). Последняя строка не комментарий в файле должна быть вызовом main:
main "$@"
Очевидно, что для коротких скриптов, которые представляют ссобой лишь линейный поток, функция main
— излишняя, и поэтому не требуется.
Проверка возвращаемых значений
Всегда проверяйте возвращаемые значения и дайте информативные возвращаемые значения.
Для команд не использующих пайплайн используйте $?
или провяйте непосредственно через оператор if
, чтобы было проще.
Пример:
if ! mv "${file_list}" "${dest_dir}/" ; then
echo "Unable to move ${file_list} to ${dest_dir}" >&2
exit "${E_BAD_MOVE}"
fi
# Или
mv "${file_list}" "${dest_dir}/"
if [[ "$?" -ne 0 ]]; then
echo "Unable to move ${file_list} to ${dest_dir}" >&2
exit "${E_BAD_MOVE}"
fi
Bash также имеет переменную `PIPESTATUS`, которая позволяет проверять код возврата со всех частей пайплайна. Если необходимо проверить усшно ли завершен или произошел отказ всего пайпа, то приемлемо следующее:
```bash
tar -cf - ./* | ( cd "${dir}" && tar -xf - )
if [[ "${PIPESTATUS[0]}" -ne 0 || "${PIPESTATUS[1]}" -ne 0 ]]; then
echo "Unable to tar files to ${dir}" >&2
fi
Однако, так как PIPESTATUS
будет перезаписан, сразу как только вы выполните какую-либо другую команду, если вам действительно нужно обработать все события где произошла ошибка в пайплайне, необходимо переназначить PIPESTATUS
другой переменной сразу после запуска команды (не забывайте, что [
является командой и уничтожит PIPESTATUS
).
tar -cf - ./* | ( cd "${DIR}" && tar -xf - )
return_codes=(${PIPESTATUS[*]})
if [[ "${return_codes[0]}" -ne 0 ]]; then
do_something
fi
if [[ "${return_codes[1]}" -ne 0 ]]; then
do_something_else
fi
Встроенные функции или Внешние комманды
Делая выбор между вызовом встроенной в shell функции и вызовом отдельного процесса, выберите встроенный.
Мы предпочитаем использование встроенных функций, таких как функции расширения параметров в bash, поскольку они более надежны и переносимы (особенно по сравнению с такими, как sed
).
Пример:
# Предпочитайте это:
addition=$((${X} + ${Y}))
substitution="${string/#foo/bar}"
# Вместо этого:
addition="$(expr ${X} + ${Y})"
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"
Используйте здравый смысл и БУДЬТЕ КОНСИСТЕНТНЫ.
Пожалуйста уделите несколько минут, чтобы прочитать раздел Parting Words в нижней части руководства C++.