Большинство языков программирования имеют набор «лучших практик», которые следует соблюдать при написании кода на этом языке. Тем не менее, я не смог найти исчерпывающего описания сценариев оболочки, поэтому решил написать свой собственный, основываясь на своем опыте написания сценариев оболочки за эти годы.
Замечание о переносимости : так как я в основном пишу сценарии оболочки для запуска в системах, в которых установлен Bash 4.2, мне не нужно сильно беспокоиться о переносимости, но вам может понадобиться! Список ниже написан с учетом Bash 4.2 (и других современных оболочек). Если вы пишете переносимый скрипт, некоторые пункты не будут применяться. Само собой разумеется, вы должны выполнить достаточное тестирование после внесения любых изменений на основе этого списка.
Вот мой список лучших практик для сценариев оболочки (в произвольном порядке):
- Используйте функции
- Документируйте свои функции
- Используйте
shiftдля чтения аргументов функции - Объявите свои переменные
- Укажите все расширения параметров
- При необходимости используйте массивы
- Используйте
"$@"для ссылки на все аргументы - Используйте имена переменных в верхнем регистре только для переменных среды
- Предпочитают встроенные функции оболочки над внешними программами
- Избегайте ненужных трубопроводов
- Избегайте разбора
ls - Используйте шатание
- Используйте вывод с нулевым разделителем, где это возможно
- Не используйте спины
- Используйте замену процесса вместо создания временных файлов
- Используйте
mktempесли вам нужно создавать временные файлы - Используйте
[[и((для условий тестирования - Используйте команды в тестовых условиях вместо состояния выхода
- Используйте
set -e - Пишите сообщения об ошибках в stderr
Каждый из пунктов выше описан в некоторых деталях ниже.
- Используйте функции
Если вы не пишете очень маленький сценарий, используйте функции для модульного кодирования и сделайте его более читабельным, многоразовым и обслуживаемым. Шаблон, который я использую для всех моих скриптов, показан ниже. Как видите, весь код написан внутри функций. Сценарий начинается с вызова
mainфункции.01020304050607080910111213#!/bin/bashset-eusage() {}my_function() {}main() {}main"$@" - Документируйте свои функции
Добавьте достаточное количество документации к своим функциям, чтобы указать, что они делают и какие аргументы необходимы для их вызова. Вот пример:
12345# Processes a file.# $1 - the name of the input file# $2 - the name of the output fileprocess_file(){} - Используйте
shiftдля чтения аргументов функцииВместо использования
$1,$2т. Д. Для выбора аргументов функции используйтеshiftкак показано ниже. Это упрощает изменение порядка аргументов, если вы передумаете позже.1234567# Processes a file.# $1 - the name of the input file# $2 - the name of the output fileprocess_file(){local-r input_file="$1";shiftlocal-r output_file="$1";shift} - Объявите свои переменные
Если ваша переменная является целым числом, объявите ее как таковую. Кроме того, делайте все свои переменные
readonlyтолько дляreadonlyесли только вы не намереваетесь изменить их значение позже в своем скрипте. Используйтеlocalдля переменных, объявленных внутри функций. Это помогает передать ваши намерения . Если переносимость является проблемой, используйтеtypesetвместоdeclare. Вот несколько примеров:123456declare-r -i port_number=8080declare-r -a my_array=( apple orange )my_function() {local-r name=apple} - Укажите все расширения параметров
Чтобы предотвратить расщепление слов и искажение файлов, вы должны заключить в кавычки все расширения переменных. В частности, вы должны сделать это, если вы имеете дело с именами файлов, которые могут содержать пробелы (или другие специальные символы). Рассмотрим этот пример:
01020304050607080910111213141516171819202122# create a file containing a space in its nametouch"foo bar"declare-r my_file="foo bar"# try rm-ing the file without quoting the variablerm$my_file# it fails because rm sees two arguments: "foo" and "bar"# rm: cannot remove `foo': No such file or directory# rm: cannot remove `bar': No such file or directory# need to quote the variablerm"$my_file"# file globbing example:mesg="my pattern is *.txt"echo$mesg# this is not quoted so *.txt will undergo expansion# will print "my pattern is foo.txt bar.txt"# need to quote it for correct outputecho"$msg"Хорошей практикой является цитирование всех ваших переменных. Если вам нужно разделить слова, рассмотрите возможность использования массива. Смотрите следующий пункт.
- При необходимости используйте массивы
Не храните коллекцию элементов в строке. Вместо этого используйте массив. Например:
01020304050607080910111213# using a string to hold a collectiondeclare-r hosts="host1 host2 host3"forhostin$hosts# not quoting $hosts here, since we want word splittingdoecho"$host"done# use an array instead!declare-r -a host_array=( host1 host2 host3 )forhostin"${host_array[@]}"doecho"$host"done - Используйте
"$@"для ссылки на все аргументыНе используйте
$*. Обратитесь к моему предыдущему сообщению: разница между $ *, $ @, «$ *» и «$ @» . Вот пример:123456789main() {# print each argumentforiin"$@"doecho"$i"done}# pass all arguments to mainmain"$@" - Используйте имена переменных в верхнем регистре только для переменных ENVIRONMENT
Лично я предпочитаю, чтобы все переменные были строчными, кроме переменных среды. Например:
1234declare-i port_number=8080# JAVA_HOME and CLASSPATH are environment variables"$JAVA_HOME"/bin/java-cp"$CLASSPATH"app.Main"$port_number" - Предпочитаю встроенные функции оболочки над внешними программами
Оболочка имеет возможность манипулировать строками и выполнять простую арифметику, поэтому вам не нужно вызывать такие программы, как
cutиsed. Вот несколько примеров:01020304050607080910111213141516171819declare-r my_file="/var/tmp/blah"# instead of dirname, use:declare-r file_dir="{my_file%/*}"# instead of basename, use:declare-r file_base="{my_file##*/}"# instead of sed 's/blah/hello', use:declare-r new_file="${my_file/blah/hello}"# instead of bc <<< "2+2", use:echo$(( 2+2 ))# instead of grepping a pattern in a string, use:[[ $line =~ .*blah$ ]]# instead of cut -d:, use an array:IFS=:read-a arr <<<"one:two:three"Обратите внимание, что внешняя программа будет работать лучше при работе с большими файлами / вводом.
- Избегайте ненужных трубопроводов
Конвейеры добавляют дополнительную нагрузку на ваш скрипт, поэтому старайтесь, чтобы ваши конвейеры были небольшими. Типичными примерами бесполезных конвейеров являются
catиecho, показанные ниже:- Избегайте ненужных
catЕсли вы не знакомы с печально известной наградой «Бесполезное использование кошек», посмотрите здесь . Команда
catдолжна использоваться только для объединения файлов, но не для отправки вывода файла другой команде.1234# instead ofcatfile|command# usecommand<file - Избегайте ненужного
echoВы должны использовать
echoтолько если вы хотите вывести некоторый текст в stdout, stderr, файл и т. Д. Если вы хотите отправить текст другой команде, неechoего через канал! Вместо этого используйте здесь-строку. Обратите внимание, что здесь строки не являются переносимыми (но большинство современных оболочек поддерживают их), поэтому используйте heredoc, если вы пишете переносимый скрипт. (См. Мой предыдущий пост: Бесполезное использование эха .)123456789# instead ofechotext |command# usecommand<<< text# for portability, use a heredoccommand<< ENDtextEND - Избегайте ненужных
grepПересылка из
grepвawkилиsedне нужна. Посколькуawkиsedмогут работать сgrep, вам не нуженgrepв вашем конвейере. (Проверьте мой предыдущий пост: Бесполезное использование Grep .)123456789# instead ofgreppatternfile|awk'{print $1}'# useawk'/pattern/{print $1}'# instead ofgreppatternfile|sed's/foo/bar/g'# usesed-n'/pattern/{s/foo/bar/p}'file - Другие ненужные трубопроводы
Вот несколько других примеров:
123456789# instead ofcommand|sort|uniq# usecommand|sort-u# instead ofcommand|greppattern |wc-l# usecommand|grep-c pattern
- Избегайте ненужных
- Избегайте разбора
lsПроблема в том, что
lsвыводит имена файлов, разделенные символами новой строки, поэтому если у вас есть имя файла, содержащее символ новой строки, вы не сможете его правильно проанализировать. Было бы неплохо, если быlsмог выводить имена файлов, разделенные нулем, но, к сожалению, не может. Вместоlsиспользуйте «globbing» файла или альтернативную команду, которая выводит имена файлов, оканчивающиеся нулем, такие какfind -print0. - Используйте шатание
Глобализация (или расширение имени файла) — это способ оболочки создать список файлов, соответствующих шаблону. В bash вы можете сделать глобализацию более мощной, включив расширенные операторы сопоставления с шаблоном, используя
extglobоболочкиextglob. Также включитеnullglobчтобы получить пустой список, если совпадений не найдено. Globbing можно использовать вместоfindв некоторых случаях и, опять же, не анализироватьls! Вот пара примеров:12345678shopt-s nullglobshopt-s extglob# get all files with a .yyyymmdd.txt suffixdeclare-a dated_files=( *.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].txt )# get all non-zip filesdeclare-a non_zip_files=( !(*.zip) ) - Используйте вывод с нулевым разделителем, где это возможно
Чтобы правильно обрабатывать имена файлов, содержащие пробельные символы и символы новой строки, вы должны использовать вывод с разделителями, равными нулю, в результате чего каждая строка заканчивается символом NUL (
00) вместо новой строки. Большинство программ поддерживают это. Например,find -print0выводит имена файлов, за которыми следует нулевой символ, аxargs -0читает аргументы, разделенные нулевыми символами.123456789# instead offind. -typef -mtime +5 |xargsrm-f# usefind. -typef -mtime +5 -print0 |xargs-0rm-f# looping over filesfind. -typef -print0 |whileIFS=read-r -d $''filename;doecho"$filename"done - Не используйте спины
Используйте
$(command)вместо`command`потому что это упрощает`command`нескольких команд и делает ваш код более читабельным. Вот простой пример:12345# ugly escaping required when using nested backticksa=`command1 \`command2\``# $(...) is cleanerb=$(command1 $(command2)) - Используйте замену процесса вместо создания временных файлов
В большинстве случаев, если команда принимает файл в качестве ввода, файл может быть заменен выводом другой команды, используя подстановку процесса:
<(command). Это избавляет вас от необходимости записывать временный файл, передавать этот временный файл команде и, наконец, удалять временный файл. Это показано ниже:12345678# using temp filescommand1 > file1command2 > file2difffile1 file2rmfile1 file2# using process substitutiondiff<(command1) <(command2) - Используйте
mktempесли вам нужно создавать временные файлыСтарайтесь избегать создания временных файлов. Если необходимо, используйте
mktempдля создания временного каталога, а затем запишите в него свои файлы. Убедитесь, что вы удалите каталог после того, как вы закончите.123456789# set up a trap to delete the temp dir when the script exitsunsettemp_dirtrap'[[ -d "$temp_dir" ]] && rm -rf "$temp_dir"'EXIT# create the temp dirdeclare-r temp_dir=$(mktemp -dt myapp.XXXXXX)# write to the temp dircommand>"$temp_dir"/foo - Используйте
[[и((для условий тестированияПредпочитайте
[[ ... ]]над[ ... ]потому что это безопаснее и обеспечивает более богатый набор функций. Используйте(( ... ))для арифметических условий, потому что это позволяет вам выполнять сравнения, используя знакомые математические операторы, такие как<и>вместо-ltи-gt. Обратите внимание, что если вы хотите мобильности, вы должны придерживаться старомодного[ ... ]. Вот несколько примеров:12345[[ $foo =="foo"]] &&echo"match"# don't need to quote variable inside [[[[ $foo =="a"&& $bar =="a"]] &&echo"match"declare-i num=5(( num < 10 )) &&echo"match"# don't need the $ on $num in (( - Используйте команды в тестовых условиях вместо состояния выхода
Если вы хотите проверить, была ли команда выполнена успешно, прежде чем что-то делать, используйте команду непосредственно в условии вашего оператора if вместо проверки состояния завершения команды.
010203040506070809101112# don't use exit statusgrep-q patternfileif(( $? == 0 ))thenecho"pattern was found"fi# use the command as the conditionifgrep-q patternfilethenecho"pattern was found"fi - Используйте
set -eПоместите это в верхней части вашего сценария. Это говорит оболочке выйти из скрипта, как только любой оператор вернет ненулевой код завершения.
- Пишите сообщения об ошибках в stderr
Сообщения об ошибках относятся к stderr, а не к stdout.1echo"An error message">&2