Подтвердить что ты не робот

Шаблоны сценариев оболочки

Что бы вы посоветовали для хорошего шаблона сценария bash/ksh, который будет использоваться в качестве стандарта для всех вновь создаваемых сценариев?

Я обычно начинаю (после строки #!) с закомментированного заголовка с именем файла, кратким описанием, использованием, возвращаемыми значениями, автором (ами), журналом изменений и помещаюсь в строки из 80 символов.

Все строки документации я начинаю с двойных символов хеша ##, поэтому я могу легко их найти, а локальные имена переменных начинаются с "__".

Любые другие лучшие практики? Подсказки? Соглашения об именах? Как насчет кодов возврата?

Комментарии к управлению версиями: у нас все в порядке с SVN, но у другого отдела на предприятии есть отдельный репозиторий, и это их сценарий. Как мне узнать, с кем связаться с Q, если нет информации @author? Использование записей, похожих на javadocs, имеет смысл даже в контексте оболочки, IMHO, но я могу ошибаться.

4b9b3361

Ответ 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 интерпретируемые символы (или любой интерпретируемый символ) в именах файлов или любое другое имя. в частности... "'` $и * #() {} [] -, я использую _ для пробелов.

Помните, что это просто соглашения. Лучшая практика, грубая, но иногда вы вынуждены выходить за пределы. Самое главное - быть последовательным в рамках ваших проектов.