Шаблоны сценариев оболочки
Что бы вы посоветовали для хорошего шаблона сценария bash/ksh, который будет использоваться в качестве стандарта для всех вновь создаваемых сценариев?
Я обычно начинаю (после строки #!
) с закомментированного заголовка с именем файла, кратким описанием, использованием, возвращаемыми значениями, автором (ами), журналом изменений и помещаюсь в строки из 80 символов.
Все строки документации я начинаю с двойных символов хеша ##
, поэтому я могу легко их найти, а локальные имена переменных начинаются с "__".
Любые другие лучшие практики? Подсказки? Соглашения об именах? Как насчет кодов возврата?
Комментарии к управлению версиями: у нас все в порядке с SVN, но у другого отдела на предприятии есть отдельный репозиторий, и это их сценарий. Как мне узнать, с кем связаться с Q, если нет информации @author? Использование записей, похожих на javadocs, имеет смысл даже в контексте оболочки, IMHO, но я могу ошибаться.
Ответы
Ответ 1
Я бы расширил нормандский ответ до 6 строк, и последняя из них будет пустой:
#!/bin/ksh
#
# @(#)$Id$
#
# Purpose
Третья строка - это строка идентификации управления версиями - на самом деле это гибрид с маркером SCCS '@(#)
', который может быть идентифицирован программой (SCCS) what
, и строкой версии RCS, которая раскрывается при файл помещен под RCS, VCS по умолчанию, которую я использую для личного пользования. Программа RCS ident
принимает расширенную форму $Id$
, которая может выглядеть как $Id: mkscript.sh,v 2.3 2005/05/20 21:06:35 jleffler Exp $
. Пятая строка напоминает мне, что скрипт должен иметь описание своего назначения вверху; Я заменяю слово фактическим описанием сценария (например, после него нет двоеточия).
После этого, по существу, нет ничего стандартного для сценария оболочки. Появляются стандартные фрагменты, но в каждом скрипте нет стандартного фрагмента. (В моем обсуждении предполагается, что сценарии написаны в нотациях оболочки Bourne, Korn или POSIX (Bash). Существует отдельное обсуждение того, почему любой, кто помещает производную C Shell после символа #!
, живет во грехе.)
Например, этот код появляется в той или иной форме всякий раз, когда скрипт создает промежуточные (временные) файлы:
tmp=${TMPDIR:-/tmp}/prog.$$
trap "rm -f $tmp.?; exit 1" 0 1 2 3 13 15
...real work that creates temp files $tmp.1, $tmp.2, ...
rm -f $tmp.?
trap 0
exit 0
В первой строке выбирается временный каталог, по умолчанию используется значение /tmp, если пользователь не указал альтернативу ($ TMPDIR очень широко признан и стандартизирован POSIX). Затем он создает префикс имени файла, включая идентификатор процесса. Это не мера безопасности; это простая мера параллелизма, предотвращающая попадание нескольких экземпляров сценария друг на друга данных. (В целях безопасности используйте непредсказуемые имена файлов в непубличном каталоге.) Вторая строка обеспечивает выполнение команд "rm
" и "exit
", если оболочка получает какой-либо из сигналов SIGHUP (1), SIGINT (2), SIGQUIT (3), SIGPIPE (13) или SIGTERM (15). Команда 'rm
' удаляет все промежуточные файлы, которые соответствуют шаблону; команда exit
обеспечивает ненулевое состояние, что указывает на какую-то ошибку. "trap
", равное 0, означает, что код также выполняется, если оболочка выходит по какой-либо причине, - это покрывает небрежность в разделе, помеченном как "реальная работа". Затем код в конце удаляет все оставшиеся временные файлы перед снятием ловушки при выходе и, наконец, завершается с нулевым (успешным) статусом. Очевидно, что если вы хотите выйти с другим состоянием, вы можете - просто убедитесь, что вы установили его в переменную, прежде чем запускать строки rm
и trap
, а затем используйте exit $exitval
.
Я обычно использую следующее для удаления пути и суффикса из скрипта, поэтому я могу использовать $arg0
при сообщении об ошибках:
arg0=$(basename $0 .sh)
Я часто использую функцию оболочки для сообщения об ошибках:
error()
{
echo "$arg0: $*" 1>&2
exit 1
}
Если есть только одна или две ошибки, я не беспокоюсь об этой функции; если есть еще, я делаю, потому что это упрощает кодирование. Я также создаю более или менее сложные функции под названием usage
, чтобы дать краткую информацию о том, как использовать команду - опять же, только если есть несколько мест, где она будет использоваться.
Другой довольно стандартный фрагмент - это цикл синтаксического анализа параметров, использующий встроенную оболочку getopts
:
vflag=0
out=
file=
Dflag=
while getopts hvVf:o:D: flag
do
case "$flag" in
(h) help; exit 0;;
(V) echo "$arg0: version $Revision$ ($Date$)"; exit 0;;
(v) vflag=1;;
(f) file="$OPTARG";;
(o) out="$OPTARG";;
(D) Dflag="$Dflag $OPTARG";;
(*) usage;;
esac
done
shift $(expr $OPTIND - 1)
или:
shift $(($OPTIND - 1))
Кавычки вокруг "$ OPTARG" обрабатывают пробелы в аргументах. Dflag является кумулятивным, но используемые здесь обозначения теряют пробелы в аргументах. Существуют (нестандартные) способы решения этой проблемы.
Первая нотация сдвига работает с любой оболочкой (или сработает, если бы я использовал обратные галочки вместо '$(...)
'). Вторая работает в современных оболочках; может даже быть альтернатива с квадратными скобками вместо скобок, но это работает так Я не удосужился разобраться, что это такое.
Последний трюк на данный момент заключается в том, что у меня часто есть как программы GNU, так и не-GNU версии, и я хочу иметь возможность выбирать, какую использовать. Поэтому во многих моих сценариях используются такие переменные, как:
: ${PERL:=perl}
: ${SED:=sed}
И затем, когда мне нужно вызвать Perl или sed
, скрипт использует $PERL
или $SED
. Это помогает мне, когда что-то ведет себя по-другому - я могу выбрать рабочую версию - или при разработке сценария (я могу добавить дополнительные опции только для отладки в команду без изменения сценария). (См. Расширение параметров оболочки для получения информации о ${VAR:=value}
и связанных примечаниях.)
Ответ 2
Я использую первый набор строк ## для документации по использованию. Я не могу вспомнить, где я впервые увидел это.
#!/bin/sh
## Usage: myscript [options] ARG1
##
## Options:
## -h, --help Display this message.
## -n Dry-run; only show what would be done.
##
usage() {
[ "$*" ] && echo "$0: $*"
sed -n '/^##/,/^$/s/^## \{0,1\}//p' "$0"
exit 2
} 2>/dev/null
main() {
while [ $# -gt 0 ]; do
case $1 in
(-n) DRY_RUN=1;;
(-h|--help) usage 2>&1;;
(--) shift; break;;
(-*) usage "$1: unknown option";;
(*) break;;
esac
done
: do stuff.
}
Ответ 3
Любой код, который будет выпущен в дикой природе, должен иметь следующий короткий заголовок:
# Script to turn lead into gold
# Copyright (C) 2009 Ima Hacker ([email protected])
# Permission to copy and modify is granted under the foo license
# Last revised 1/1/2009
Ведение журнала изменений в заголовках кода - это возврат к тому моменту, когда системы контроля версий были ужасно неудобны. Дата последнего изменения показывает кому-то, сколько лет сценарию.
Если вы собираетесь полагаться на bashisms, используйте #!/Bin/bash, а не /bin/sh, так как sh - это вызов POSIX любой оболочки. Даже если /bin/sh указывает на bash, многие функции будут отключены, если вы запустите его через /bin/sh. Большинство дистрибутивов Linux не принимают сценарии, которые основаны на bashisms, попробуйте быть переносимыми.
Для меня комментарии в сценариях оболочки глупы, если они не читают что-то вроде:
# I am not crazy, this really is the only way to do this
Сценарии оболочки настолько просты, что (если вы не пишете демонстрацию, чтобы научить кого-то, как это делать), код почти всегда объясняет себя.
Некоторым оболочкам не нравится, когда их кормят типичными локальными переменными. Я считаю, что по сей день Busybox (обычная спасательная оболочка) является одним из них. Вместо этого сделайте GLOBALS_OBVIOUS, чтобы его было легче читать, особенно при отладке через /bin/sh -x./script.sh.
Мое личное предпочтение - позволить логике говорить за себя и минимизировать работу парсера. Например, многие могут написать:
if [ $i = 1 ]; then
... some code
fi
Где бы я просто:
[ $i = 1 ] && {
... some code
}
Аналогично, кто-то может написать:
if [ $i -ne 1 ]; then
... some code
fi
... где бы я:
[ $i = 1 ] || {
... some code
}
Единственный раз, когда я использую обычный if/then/else, это если есть else-if, чтобы добавить микс.
Ужасно безумный пример очень хорошего переносимого кода оболочки можно изучить, просто просмотрев скрипт "configure" в большинстве бесплатных программных пакетов, использующих autoconf. Я говорю "безумие", потому что его 6300 строк кода обслуживают каждую известную человечеству систему с оболочкой в стиле UNIX. Вы не хотите такого раздувания, но интересно изучить некоторые из различных хаков переносимости в пределах.. таких как быть хорошим для тех, кто может указать /bin/sh на zsh :)
Единственный другой совет, который я могу дать, - следить за расширением в документах здесь, т.е.
cat << EOF > foo.sh
printf "%s was here" "$name"
EOF
... собирается расширить $ name, когда вы, вероятно, захотите оставить переменную на месте. Решите это через:
printf "%s was here" "\$name"
который оставит $ name в качестве переменной, а не расширяет ее.
Я также настоятельно рекомендую научиться использовать ловушку для перехвата сигналов... и использовать эти обработчики в качестве стандартного кода. Говорить, что работающий скрипт замедляется с помощью простого SIGUSR1, очень удобно :)
Большинство новых программ, которые я пишу (ориентированных на инструмент/командную строку), начинаются как сценарии оболочки, это отличный способ для прототипирования инструментов UNIX.
Вам также может понравиться компилятор сценариев оболочки SHC, посмотрите его здесь.
Ответ 4
Это заголовок, который я использую для оболочки script (bash или ksh).
Это внешний вид man
, и он также используется для отображения использования().
#!/bin/ksh
#================================================================
# HEADER
#================================================================
#% SYNOPSIS
#+ ${SCRIPT_NAME} [-hv] [-o[file]] args ...
#%
#% DESCRIPTION
#% This is a script template
#% to start any good shell script.
#%
#% OPTIONS
#% -o [file], --output=[file] Set log file (default=/dev/null)
#% use DEFAULT keyword to autoname file
#% The default value is /dev/null.
#% -t, --timelog Add timestamp to log ("+%y/%m/%[email protected]%H:%M:%S")
#% -x, --ignorelock Ignore if lock file exists
#% -h, --help Print this help
#% -v, --version Print script information
#%
#% EXAMPLES
#% ${SCRIPT_NAME} -o DEFAULT arg1 arg2
#%
#================================================================
#- IMPLEMENTATION
#- version ${SCRIPT_NAME} (www.uxora.com) 0.0.4
#- author Michel VONGVILAY
#- copyright Copyright (c) http://www.uxora.com
#- license GNU General Public License
#- script_id 12345
#-
#================================================================
# HISTORY
# 2015/03/01 : mvongvilay : Script creation
# 2015/04/01 : mvongvilay : Add long options and improvements
#
#================================================================
# DEBUG OPTION
# set -n # Uncomment to check your syntax, without execution.
# set -x # Uncomment to debug this shell script
#
#================================================================
# END_OF_HEADER
#================================================================
И вот функции использования:
#== needed variables ==#
SCRIPT_HEADSIZE=$(head -200 ${0} |grep -n "^# END_OF_HEADER" | cut -f1 -d:)
SCRIPT_NAME="$(basename ${0})"
#== usage functions ==#
usage() { printf "Usage: "; head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#+" | sed -e "s/^#+[ ]*//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g" ; }
usagefull() { head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#[%+-]" | sed -e "s/^#[%+-]//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g" ; }
scriptinfo() { head -${SCRIPT_HEADSIZE:-99} ${0} | grep -e "^#-" | sed -e "s/^#-//g" -e "s/\${SCRIPT_NAME}/${SCRIPT_NAME}/g"; }
Вот что вы должны получить:
# Display help
$ ./template.sh --help
SYNOPSIS
template.sh [-hv] [-o[file]] args ...
DESCRIPTION
This is a script template
to start any good shell script.
OPTIONS
-o [file], --output=[file] Set log file (default=/dev/null)
use DEFAULT keyword to autoname file
The default value is /dev/null.
-t, --timelog Add timestamp to log ("+%y/%m/%[email protected]%H:%M:%S")
-x, --ignorelock Ignore if lock file exists
-h, --help Print this help
-v, --version Print script information
EXAMPLES
template.sh -o DEFAULT arg1 arg2
IMPLEMENTATION
version template.sh (www.uxora.com) 0.0.4
author Michel VONGVILAY
copyright Copyright (c) http://www.uxora.com
license GNU General Public License
script_id 12345
# Display version info
$ ./template.sh -v
IMPLEMENTATION
version template.sh (www.uxora.com) 0.0.4
author Michel VONGVILAY
copyright Copyright (c) http://www.uxora.com
license GNU General Public License
script_id 12345
Вы можете получить полный шаблон script здесь: http://www.uxora.com/unix/shell-script/18-shell-script-template
Ответ 5
Включение обнаружения ошибок значительно облегчает обнаружение проблем в script в начале:
set -o errexit
Закройте script при первой ошибке. Таким образом, вы избегаете продолжать делать что-то, что зависело от чего-то ранее в script, возможно, в конечном итоге с каким-то странным состоянием системы.
set -o nounset
Лечить ссылки на неустановленные переменные как ошибки. Очень важно избегать запуска таких функций, как rm -you_know_what "$var/"
с помощью unset $var
. Если вы знаете, что переменная может быть отключена, и это безопасная ситуация, вы можете использовать ${var-value}
для использования другого значения, если оно отменено или ${var:-value}
использовать другое значение, если оно не установлено или пусто.
set -o noclobber
Легко сделать ошибку при вставке >
, где вы хотели вставить <
, и перезаписать некоторый файл, который вы хотели прочитать. Если вам нужно сжать файл в script, вы можете отключить его до соответствующей строки и снова включить его после.
set -o pipefail
Используйте первый ненулевой код выхода (если таковой имеется) из набора управляемой команды в качестве кода завершения полного набора команд. Это облегчает отладку команд с каналами.
shopt -s nullglob
Избегайте, чтобы ваш /foo/*
glob интерпретировался буквально, если нет файлов, соответствующих этому выражению.
Вы можете объединить все эти строки в две строки:
set -o errexit -o nounset -o noclobber -o pipefail
shopt -s nullglob
Ответ 6
Мой шаблон bash выглядит следующим образом (установлен в моей конфигурации vim):
#!/bin/bash
## DESCRIPTION:
## AUTHOR: $USER_FULLNAME
declare -r SCRIPT_NAME=$(basename "$BASH_SOURCE" .sh)
## exit the shell(default status code: 1) after printing the message to stderr
bail() {
echo -ne "$1" >&2
exit ${2-1}
}
## help message
declare -r HELP_MSG="Usage: $SCRIPT_NAME [OPTION]... [ARG]...
-h display this help and exit
"
## print the usage and exit the shell(default status code: 2)
usage() {
declare status=2
if [[ "$1" =~ ^[0-9]+$ ]]; then
status=$1
shift
fi
bail "${1}$HELP_MSG" $status
}
while getopts ":h" opt; do
case $opt in
h)
usage 0
;;
\?)
usage "Invalid option: -$OPTARG \n"
;;
esac
done
shift $(($OPTIND - 1))
[[ "$#" -lt 1 ]] && usage "Too few arguments\n"
#==========MAIN CODE BELOW==========
Ответ 7
Я бы предложил
#!/bin/ksh
и что он. Замечания блока тяжелого веса для сценариев оболочки? Я получаю визы.
Предложения:
-
Документация должна быть данными или кодом, а не комментариями. По крайней мере, функция usage()
. Посмотрите, как ksh и другие инструменты AST документируют себя с параметрами -man для каждой команды. (Не удается связать, поскольку веб-сайт не работает.)
-
Объявить локальные переменные с помощью typeset
. Это для чего. Нет необходимости в неприятных подчеркиваниях.
Ответ 8
Что вы можете сделать, так это сделать script, который создает заголовок для script и автоматически его откроет в вашем любимом редакторе. Я видел, как парень сделал это на этом сайте:
http://code.activestate.com/recipes/577862-bash-script-to-create-a-header-for-bash-scripts/?in=lang-bash
#!/bin/bash -
#title :mkscript.sh
#description :This script will make a header for a bash script.
#author :your_name_here
#date :20110831
#version :0.3
#usage :bash mkscript.sh
#notes :Vim and Emacs are needed to use this script.
#bash_version :4.1.5(1)-release
#===============================================================================
Ответ 9
Как правило, у меня есть несколько соглашений, которые мне нравятся для каждого script, который я пишу.
Я пишу все сценарии с предположением, что другие люди могут их прочитать.
Я начинаю каждый script с моего заголовка,
#!/bin/bash
# [ID LINE]
##
## FILE: [Filename]
##
## DESCRIPTION: [Description]
##
## AUTHOR: [Author]
##
## DATE: [XX_XX_XXXX.XX_XX_XX]
##
## VERSION: [Version]
##
## USAGE: [Usage]
##
Я использую этот формат даты, чтобы упростить grep/search.
Я использую "[фигурные скобки", чтобы указать, что люди должны сами вводить себя.
если они встречаются вне комментария, я пытаюсь запустить их с помощью # #.
Таким образом, если кто-то вставляет их как есть, это не будет ошибкой для ввода или команды тестирования. Проверьте раздел использования на странице руководства, чтобы увидеть этот стиль в качестве примера.
Когда я хочу прокомментировать строку кода, я использую сингл '#'. Когда я делаю комментарий как примечание, я использую double '##'. /etc/nanorc
также использует это соглашение. Я нахожу это полезным, чтобы отличить комментарий, который был выбран не для выполнения; стихи комментарий, который был создан как примечание.
Все мои переменные оболочки, я предпочитаю делать в CAPS. Я стараюсь держать от 4 до 8 символов, если не требуется иное. Имена связывают, насколько это возможно, с их использованием.
Я также всегда выхожу с 0 в случае успеха или 1 для ошибок. Если script имеет много разных типов ошибок (и фактически поможет кому-то или может быть использован каким-то образом в каком-то коде), я бы выбрал документированную последовательность над 1.
В общем случае коды выхода не так строго соблюдаются в мире * nix. К сожалению, я никогда не нашел хорошей общей схемы номеров.
Мне нравится обрабатывать аргументы стандартным образом. Я всегда предпочитаю getopts, getopt. Я никогда не делаю взлома с командами "read" и операторами if. Я также хотел бы использовать оператор case, чтобы избежать вложенных ifs. Я использую перевод script для длинных опций, поэтому -help означает -h для getopts. Я пишу все сценарии в bash (если это приемлемо) или generic sh.
Я НИКОГДА не использую bash интерпретируемые символы (или любой интерпретируемый символ) в именах файлов или любое другое имя.
в частности... "'` $и * #() {} [] -, я использую _ для пробелов.
Помните, что это просто соглашения. Лучшая практика, грубая, но иногда вы вынуждены выходить за пределы. Самое главное - быть последовательным в рамках ваших проектов.