Шаблоны проектирования или рекомендации по сценариям оболочки

Кто-нибудь знает какие-либо ресурсы, которые рассказывают о лучших практиках или шаблонах проектирования для сценариев оболочки (sh, bash и т.д.)?

Ответ 1

Я написал довольно сложные сценарии оболочки, и мое первое предложение - "не делать". Причина в том, что довольно легко сделать небольшую ошибку, которая мешает вашему script, или даже сделать его опасным.

Тем не менее, у меня нет других ресурсов, чтобы передать вам, но мой личный опыт. Вот что я обычно делаю, что является излишним, но имеет тенденцию быть твердым, хотя и очень многословным.

Призвание

сделайте свой script для принятия длинных и коротких опций. будьте осторожны, потому что есть две команды для разбора опций, getopt и getopts. Используйте getopt, поскольку вы сталкиваетесь с меньшими проблемами.

CommandLineOptions__config_file=""
CommandLineOptions__debug_level=""

getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "[email protected]"`

if test $? != 0
then
    echo "unrecognized option"
    exit 1
fi

eval set -- "$getopt_results"

while true
do
    case "$1" in
        --config_file)
            CommandLineOptions__config_file="$2";
            shift 2;
            ;;
        --debug_level)
            CommandLineOptions__debug_level="$2";
            shift 2;
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "$0: unparseable option $1"
            EXCEPTION=$Main__ParameterException
            EXCEPTION_MSG="unparseable option $1"
            exit 1
            ;;
    esac
done

if test "x$CommandLineOptions__config_file" == "x"
then
    echo "$0: missing config_file parameter"
    EXCEPTION=$Main__ParameterException
    EXCEPTION_MSG="missing config_file parameter"
    exit 1
fi

Еще один важный момент в том, что программа должна всегда возвращать ноль, если успешно завершена, отличная от нуля, если что-то пошло не так.

Вызов функций

Вы можете вызывать функции в bash, просто не забудьте определить их перед вызовом. Функции похожи на скрипты, они могут возвращать только числовые значения. Это означает, что вам нужно изобрести другую стратегию для возврата строковых значений. Моя стратегия - использовать переменную с именем RESULT для хранения результата и вернуть 0, если функция завершилась чисто. Кроме того, вы можете создавать исключения, если вы возвращаете значение, отличное от нуля, и затем устанавливаете две "переменные исключений" (мои: EXCEPTION и EXCEPTION_MSG), первая из которых содержит тип исключения, а второй - человекообразное сообщение.

Когда вы вызываете функцию, параметры функции назначаются специальным vars $0, $1 и т.д. Я предлагаю вам добавить их в более значимые имена. объявить переменные внутри функции как локальные:

function foo {
   local bar="$0"
}

Ошибочные ситуации

В bash, если вы не заявляете иначе, переменная unset используется как пустая строка. Это очень опасно в случае опечатки, так как неверно типизированная переменная не будет указана, и она будет считаться пустой. использовать

set -o nounset

чтобы это не произошло. Будьте осторожны, потому что, если вы это сделаете, программа будет прерываться каждый раз, когда вы оцениваете переменную undefined. По этой причине единственным способом проверить, не определена ли переменная, является следующее:

if test "x${foo:-notset}" == "xnotset"
then
    echo "foo not set"
fi

Вы можете объявлять переменные как только для чтения:

readonly readonly_var="foo"

Модульность

Вы можете добиться модуляции "python like", если вы используете следующий код:

set -o nounset
function getScriptAbsoluteDir {
    # @description used to get the script path
    # @param $1 the script $0 parameter
    local script_invoke_path="$1"
    local cwd=`pwd`

    # absolute path ? if so, the first character is a /
    if test "x${script_invoke_path:0:1}" = 'x/'
    then
        RESULT=`dirname "$script_invoke_path"`
    else
        RESULT=`dirname "$cwd/$script_invoke_path"`
    fi
}

script_invoke_path="$0"
script_name=`basename "$0"`
getScriptAbsoluteDir "$script_invoke_path"
script_absolute_dir=$RESULT

function import() { 
    # @description importer routine to get external functionality.
    # @description the first location searched is the script directory.
    # @description if not found, search the module in the paths contained in $SHELL_LIBRARY_PATH environment variable
    # @param $1 the .shinc file to import, without .shinc extension
    module=$1

    if test "x$module" == "x"
    then
        echo "$script_name : Unable to import unspecified module. Dying."
        exit 1
    fi

    if test "x${script_absolute_dir:-notset}" == "xnotset"
    then
        echo "$script_name : Undefined script absolute dir. Did you remove getScriptAbsoluteDir? Dying."
        exit 1
    fi

    if test "x$script_absolute_dir" == "x"
    then
        echo "$script_name : empty script path. Dying."
        exit 1
    fi

    if test -e "$script_absolute_dir/$module.shinc"
    then
        # import from script directory
        . "$script_absolute_dir/$module.shinc"
    elif test "x${SHELL_LIBRARY_PATH:-notset}" != "xnotset"
    then
        # import from the shell script library path
        # save the separator and use the ':' instead
        local saved_IFS="$IFS"
        IFS=':'
        for path in $SHELL_LIBRARY_PATH
        do
            if test -e "$path/$module.shinc"
            then
                . "$path/$module.shinc"
                return
            fi
        done
        # restore the standard separator
        IFS="$saved_IFS"
    fi
    echo "$script_name : Unable to find module $module."
    exit 1
} 

вы можете импортировать файлы с расширением .shinc со следующим синтаксисом

import "AModule/ModuleFile"

Что будет искать в SHELL_LIBRARY_PATH. Поскольку вы всегда импортируете в глобальное пространство имен, не забудьте префикс всех ваших функций и переменных с правильным префиксом, иначе вы рискуете столкновениями имен. Я использую двойное подчеркивание как точку python.

Кроме того, поставьте это как первое в своем модуле

# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
    return 0
fi
BashInclude__imported=1

Объектно-ориентированное программирование

В bash вы не можете выполнять объектно-ориентированное программирование, если только не создаете довольно сложную систему распределения объектов (я думал об этом, возможно, но безумный). На практике вы, тем не менее, можете использовать "Singleton oriented programming": у вас есть один экземпляр каждого объекта и только один.

Что я делаю: я определяю объект в модуле (см. запись модуляции). Затем я определяю пустые vars (аналогичные переменным-членам) функцию init (конструктор) и функции-члены, как в этом примере код

# avoid double inclusion
if test "${Table__imported+defined}" == "defined"
then
    return 0
fi
Table__imported=1

readonly Table__NoException=""
readonly Table__ParameterException="Table__ParameterException"
readonly Table__MySqlException="Table__MySqlException"
readonly Table__NotInitializedException="Table__NotInitializedException"
readonly Table__AlreadyInitializedException="Table__AlreadyInitializedException"

# an example for module enum constants, used in the mysql table, in this case
readonly Table__GENDER_MALE="GENDER_MALE"
readonly Table__GENDER_FEMALE="GENDER_FEMALE"

# private: prefixed with p_ (a bash variable cannot start with _)
p_Table__mysql_exec="" # will contain the executed mysql command 

p_Table__initialized=0

function Table__init {
    # @description init the module with the database parameters
    # @param $1 the mysql config file
    # @exception Table__NoException, Table__ParameterException

    EXCEPTION=""
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    RESULT=""

    if test $p_Table__initialized -ne 0
    then
        EXCEPTION=$Table__AlreadyInitializedException   
        EXCEPTION_MSG="module already initialized"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
    fi


    local config_file="$1"

      # yes, I am aware that I could put default parameters and other niceties, but I am lazy today
      if test "x$config_file" = "x"; then
          EXCEPTION=$Table__ParameterException
          EXCEPTION_MSG="missing parameter config file"
          EXCEPTION_FUNC="$FUNCNAME"
          return 1
      fi


    p_Table__mysql_exec="mysql --defaults-file=$config_file --silent --skip-column-names -e "

    # mark the module as initialized
    p_Table__initialized=1

    EXCEPTION=$Table__NoException
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    return 0

}

function Table__getName() {
    # @description gets the name of the person 
    # @param $1 the row identifier
    # @result the name

    EXCEPTION=""
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    RESULT=""

    if test $p_Table__initialized -eq 0
    then
        EXCEPTION=$Table__NotInitializedException
        EXCEPTION_MSG="module not initialized"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
    fi

    id=$1

      if test "x$id" = "x"; then
          EXCEPTION=$Table__ParameterException
          EXCEPTION_MSG="missing parameter identifier"
          EXCEPTION_FUNC="$FUNCNAME"
          return 1
      fi

    local name=`$p_Table__mysql_exec "SELECT name FROM table WHERE id = '$id'"`
      if test $? != 0 ; then
        EXCEPTION=$Table__MySqlException
        EXCEPTION_MSG="unable to perform select"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
      fi

    RESULT=$name
    EXCEPTION=$Table__NoException
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    return 0
}

Сигналы отслеживания и обработки

Я нашел это полезным для обнаружения и обработки исключений.

function Main__interruptHandler() {
    # @description signal handler for SIGINT
    echo "SIGINT caught"
    exit
} 
function Main__terminationHandler() { 
    # @description signal handler for SIGTERM
    echo "SIGTERM caught"
    exit
} 
function Main__exitHandler() { 
    # @description signal handler for end of the program (clean or unclean). 
    # probably redundant call, we already call the cleanup in main.
    exit
} 

trap Main__interruptHandler INT
trap Main__terminationHandler TERM
trap Main__exitHandler EXIT

function Main__main() {
    # body
}

# catch signals and exit
trap exit INT TERM EXIT

Main__main "[email protected]"

Советы и подсказки

Если что-то не работает по какой-либо причине, попробуйте изменить порядок кода. Порядок важен и не всегда интуитивно понятен.

даже не рассматривают работу с tcsh. он не поддерживает функции, и это ужасно вообще.

Надеюсь, это поможет, хотя, пожалуйста, обратите внимание. Если вы должны использовать такие вещи, которые я написал здесь, это означает, что ваша проблема слишком сложна, чтобы ее можно было решить с помощью оболочки. используйте другой язык. Я должен был использовать его из-за человеческих факторов и наследия.

Ответ 2

Взгляните на Advanced Bash -Scripting Guide за много мудрости в сценариях оболочки - не только Bash, либо,

Не слушайте людей, говорящих вам взглянуть на другие, возможно более сложные языки. Если сценарий оболочки соответствует вашим потребностям, используйте это. Вам нужна функциональность, а не причудливость. Новые языки предоставляют ценные новые навыки для вашего резюме, но это не помогает, если у вас есть работа, которая должна быть выполнена, и вы уже знаете оболочку.

Как уже было сказано, в сценариях оболочки не так много "лучших практик" или "шаблонов проектирования". Различные применения имеют разные рекомендации и предвзятость - как и любой другой язык программирования.

Ответ 3

shell script - это язык, предназначенный для управления файлами и процессами. Хотя это здорово для этого, это не язык общего назначения, поэтому всегда старайтесь приклеить логику из существующих утилит, а не воссоздание новой логики в оболочке script.

Помимо этого общего принципа, я собрал некоторые общие ошибки оболочки script.

Ответ 5

Легко: используйте python вместо сценариев оболочки. Вы получаете почти 100-кратное увеличение удобочитаемости, не усложняя ничего, что вам не нужно, и сохраняете возможность развить части вашего script на функции, объекты, постоянные объекты (zodb), распределенные объекты (pyro) почти без дополнительного кода.

Ответ 6

используйте set -e, чтобы вы не падали вперед после ошибок. Попробуйте сделать его совместимым, не полагаясь на bash, если вы хотите, чтобы он работал на не-linux.

Ответ 7

Знайте, когда его использовать. Для быстрого и грязного склеивания вместе все в порядке. Если вам нужно сделать не более нескольких нетривиальных решений, циклы, все, перейдите на Python, Perl и modularize.

Самая большая проблема с оболочкой часто заключается в том, что конечный результат просто выглядит как большой шар грязи, 4000 строк bash и растет... и вы не можете избавиться от него, потому что от этого зависит весь ваш проект, Конечно, он начинался с 40 строк красивого bash.

Ответ 8

Чтобы найти некоторые "лучшие практики", посмотрите, как Linux-дистрибутив (например, Debian) записывает свои init-скрипты (обычно их можно найти в файле /etc/init.d)

Большинство из них не имеют "bash -isms" и имеют хорошее разделение настроек конфигурации, библиотечных файлов и форматирования исходного кода.

Мой личный стиль - написать мастер-shellscript, который определяет некоторые переменные по умолчанию, а затем пытается загрузить ( "источник" ) файл конфигурации, который может содержать новые значения.

Я стараюсь избегать функций, поскольку они усложняют задачу script. (Для этой цели был создан Perl.)

Чтобы переносить script, проверьте не только #!/bin/sh, но также используйте #!/bin/ash, #!/bin/dash и т.д. Вы увидите Bash достаточно скоро.

Ответ 9

Или более старая цитата, подобная тому, что сказал Жоао:

"Использовать perl. Вам нужно знать bash, но не использовать его."

К сожалению, я забыл, кто это сказал.

И да, в эти дни я бы порекомендовал python над perl.