Большинство языков программирования имеют набор «лучших практик», которые следует соблюдать при написании кода на этом языке. Тем не менее, я не смог найти исчерпывающего описания сценариев оболочки, поэтому решил написать свой собственный, основываясь на своем опыте написания сценариев оболочки за эти годы.
Замечание о переносимости : так как я в основном пишу сценарии оболочки для запуска в системах, в которых установлен Bash 4.2, мне не нужно сильно беспокоиться о переносимости, но вам может понадобиться! Список ниже написан с учетом Bash 4.2 (и других современных оболочек). Если вы пишете переносимый скрипт, некоторые пункты не будут применяться. Само собой разумеется, вы должны выполнить достаточное тестирование после внесения любых изменений на основе этого списка.
Вот мой список лучших практик для сценариев оболочки (в произвольном порядке):
- Используйте функции
- Документируйте свои функции
- Используйте
shift
для чтения аргументов функции - Объявите свои переменные
- Укажите все расширения параметров
- При необходимости используйте массивы
- Используйте
"$@"
для ссылки на все аргументы - Используйте имена переменных в верхнем регистре только для переменных среды
- Предпочитают встроенные функции оболочки над внешними программами
- Избегайте ненужных трубопроводов
- Избегайте разбора
ls
- Используйте шатание
- Используйте вывод с нулевым разделителем, где это возможно
- Не используйте спины
- Используйте замену процесса вместо создания временных файлов
- Используйте
mktemp
если вам нужно создавать временные файлы - Используйте
[[
и((
для условий тестирования - Используйте команды в тестовых условиях вместо состояния выхода
- Используйте
set -e
- Пишите сообщения об ошибках в stderr
Каждый из пунктов выше описан в некоторых деталях ниже.
- Используйте функции
Если вы не пишете очень маленький сценарий, используйте функции для модульного кодирования и сделайте его более читабельным, многоразовым и обслуживаемым. Шаблон, который я использую для всех моих скриптов, показан ниже. Как видите, весь код написан внутри функций. Сценарий начинается с вызова
main
функции.01020304050607080910111213#!/bin/bash
set
-e
usage() {
}
my_function() {
}
main() {
}
main
"$@"
- Документируйте свои функции
Добавьте достаточное количество документации к своим функциям, чтобы указать, что они делают и какие аргументы необходимы для их вызова. Вот пример:
12345# Processes a file.
# $1 - the name of the input file
# $2 - the name of the output file
process_file(){
}
- Используйте
shift
для чтения аргументов функцииВместо использования
$1
,$2
т. Д. Для выбора аргументов функции используйтеshift
как показано ниже. Это упрощает изменение порядка аргументов, если вы передумаете позже.1234567# Processes a file.
# $1 - the name of the input file
# $2 - the name of the output file
process_file(){
local
-r input_file=
"$1"
;
shift
local
-r output_file=
"$1"
;
shift
}
- Объявите свои переменные
Если ваша переменная является целым числом, объявите ее как таковую. Кроме того, делайте все свои переменные
readonly
только дляreadonly
если только вы не намереваетесь изменить их значение позже в своем скрипте. Используйтеlocal
для переменных, объявленных внутри функций. Это помогает передать ваши намерения . Если переносимость является проблемой, используйтеtypeset
вместоdeclare
. Вот несколько примеров:123456declare
-r -i port_number=8080
declare
-r -a my_array=( apple orange )
my_function() {
local
-r name=apple
}
- Укажите все расширения параметров
Чтобы предотвратить расщепление слов и искажение файлов, вы должны заключить в кавычки все расширения переменных. В частности, вы должны сделать это, если вы имеете дело с именами файлов, которые могут содержать пробелы (или другие специальные символы). Рассмотрим этот пример:
01020304050607080910111213141516171819202122# create a file containing a space in its name
touch
"foo bar"
declare
-r my_file=
"foo bar"
# try rm-ing the file without quoting the variable
rm
$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 variable
rm
"$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 output
echo
"$msg"
Хорошей практикой является цитирование всех ваших переменных. Если вам нужно разделить слова, рассмотрите возможность использования массива. Смотрите следующий пункт.
- При необходимости используйте массивы
Не храните коллекцию элементов в строке. Вместо этого используйте массив. Например:
01020304050607080910111213# using a string to hold a collection
declare
-r hosts=
"host1 host2 host3"
for
host
in
$hosts
# not quoting $hosts here, since we want word splitting
do
echo
"$host"
done
# use an array instead!
declare
-r -a host_array=( host1 host2 host3 )
for
host
in
"${host_array[@]}"
do
echo
"$host"
done
- Используйте
"$@"
для ссылки на все аргументыНе используйте
$*
. Обратитесь к моему предыдущему сообщению: разница между $ *, $ @, «$ *» и «$ @» . Вот пример:123456789main() {
# print each argument
for
i
in
"$@"
do
echo
"$i"
done
}
# pass all arguments to main
main
"$@"
- Используйте имена переменных в верхнем регистре только для переменных 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 of
cat
file
|
command
# use
command
<
file
- Избегайте ненужного
echo
Вы должны использовать
echo
только если вы хотите вывести некоторый текст в stdout, stderr, файл и т. Д. Если вы хотите отправить текст другой команде, неecho
его через канал! Вместо этого используйте здесь-строку. Обратите внимание, что здесь строки не являются переносимыми (но большинство современных оболочек поддерживают их), поэтому используйте heredoc, если вы пишете переносимый скрипт. (См. Мой предыдущий пост: Бесполезное использование эха .)123456789# instead of
echo
text |
command
# use
command
<<< text
# for portability, use a heredoc
command
<< END
text
END
- Избегайте ненужных
grep
Пересылка из
grep
вawk
илиsed
не нужна. Посколькуawk
иsed
могут работать сgrep
, вам не нуженgrep
в вашем конвейере. (Проверьте мой предыдущий пост: Бесполезное использование Grep .)123456789# instead of
grep
pattern
file
|
awk
'{print $1}'
# use
awk
'/pattern/{print $1}'
# instead of
grep
pattern
file
|
sed
's/foo/bar/g'
# use
sed
-n
'/pattern/{s/foo/bar/p}'
file
- Другие ненужные трубопроводы
Вот несколько других примеров:
123456789# instead of
command
|
sort
|
uniq
# use
command
|
sort
-u
# instead of
command
|
grep
pattern |
wc
-l
# use
command
|
grep
-c pattern
- Избегайте ненужных
- Избегайте разбора
ls
Проблема в том, что
ls
выводит имена файлов, разделенные символами новой строки, поэтому если у вас есть имя файла, содержащее символ новой строки, вы не сможете его правильно проанализировать. Было бы неплохо, если быls
мог выводить имена файлов, разделенные нулем, но, к сожалению, не может. Вместоls
используйте «globbing» файла или альтернативную команду, которая выводит имена файлов, оканчивающиеся нулем, такие какfind -print0
. - Используйте шатание
Глобализация (или расширение имени файла) — это способ оболочки создать список файлов, соответствующих шаблону. В bash вы можете сделать глобализацию более мощной, включив расширенные операторы сопоставления с шаблоном, используя
extglob
оболочкиextglob
. Также включитеnullglob
чтобы получить пустой список, если совпадений не найдено. Globbing можно использовать вместоfind
в некоторых случаях и, опять же, не анализироватьls
! Вот пара примеров:12345678shopt
-s nullglob
shopt
-s extglob
# get all files with a .yyyymmdd.txt suffix
declare
-a dated_files=( *.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].txt )
# get all non-zip files
declare
-a non_zip_files=( !(*.zip) )
- Используйте вывод с нулевым разделителем, где это возможно
Чтобы правильно обрабатывать имена файлов, содержащие пробельные символы и символы новой строки, вы должны использовать вывод с разделителями, равными нулю, в результате чего каждая строка заканчивается символом NUL (
00
) вместо новой строки. Большинство программ поддерживают это. Например,find -print0
выводит имена файлов, за которыми следует нулевой символ, аxargs -0
читает аргументы, разделенные нулевыми символами.123456789# instead of
find
. -
type
f -mtime +5 |
xargs
rm
-f
# use
find
. -
type
f -mtime +5 -print0 |
xargs
-0
rm
-f
# looping over files
find
. -
type
f -print0 |
while
IFS=
read
-r -d $
''
filename;
do
echo
"$filename"
done
- Не используйте спины
Используйте
$(command)
вместо`command`
потому что это упрощает`command`
нескольких команд и делает ваш код более читабельным. Вот простой пример:12345# ugly escaping required when using nested backticks
a=`command1 \`command2\``
# $(...) is cleaner
b=$(command1 $(command2))
- Используйте замену процесса вместо создания временных файлов
В большинстве случаев, если команда принимает файл в качестве ввода, файл может быть заменен выводом другой команды, используя подстановку процесса:
<(command)
. Это избавляет вас от необходимости записывать временный файл, передавать этот временный файл команде и, наконец, удалять временный файл. Это показано ниже:12345678# using temp files
command1 > file1
command2 > file2
diff
file1 file2
rm
file1 file2
# using process substitution
diff
<(command1) <(command2)
- Используйте
mktemp
если вам нужно создавать временные файлыСтарайтесь избегать создания временных файлов. Если необходимо, используйте
mktemp
для создания временного каталога, а затем запишите в него свои файлы. Убедитесь, что вы удалите каталог после того, как вы закончите.123456789# set up a trap to delete the temp dir when the script exits
unset
temp_dir
trap
'[[ -d "$temp_dir" ]] && rm -rf "$temp_dir"'
EXIT
# create the temp dir
declare
-r temp_dir=$(mktemp -dt myapp.XXXXXX)
# write to the temp dir
command
>
"$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 status
grep
-q pattern
file
if
(( $? == 0 ))
then
echo
"pattern was found"
fi
# use the command as the condition
if
grep
-q pattern
file
then
echo
"pattern was found"
fi
- Используйте
set -e
Поместите это в верхней части вашего сценария. Это говорит оболочке выйти из скрипта, как только любой оператор вернет ненулевой код завершения.
- Пишите сообщения об ошибках в stderr
Сообщения об ошибках относятся к stderr, а не к stdout.1echo
"An error message"
>&2