Шаблоны разработки или лучшие практики для [закрытых] сценариев оболочки

Как будто вы пытаетесь получить доступ к объекту, который является null. Рассмотрим ниже пример:

TypeA objA;

. В это время вы только что объявили этот объект, но не инициализировали или не инициализировали. И всякий раз, когда вы пытаетесь получить доступ к каким-либо свойствам или методам в нем, он будет генерировать NullPointerException, что имеет смысл.

См. Также этот пример:

String a = null;
System.out.println(a.toString()); // NullPointerException will be thrown
163
задан nbro 14 February 2017 в 15:47
поделиться

9 ответов

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

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

Вызов

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

CommandLineOptions__config_file=""
CommandLineOptions__debug_level=""

getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "$@"`

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), первая из которых содержит тип исключения, а вторая - читаемое человеком сообщение.

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

function foo {
   local bar="$0"
}

Ситуации, подверженные ошибкам

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

set -o nounset

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

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

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

readonly readonly_var="foo"

Модуляризация

Вы можете достичь модульности «как в Python», если будете использовать следующий код :

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. Поскольку вы всегда импортируете в глобальное пространство имен, не забудьте поставить префикс перед всеми вашими функциями и переменными, иначе вы рискуете столкнуться с именами. Я использую двойное подчеркивание в качестве точки питона.

Также, поместите это как первое в ваш модуль

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

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

В bash вы не можете заниматься объектно-ориентированным программированием, если вы не построите довольно сложную систему выделения. объектов (я думал об этом. На практике, однако, вы можете выполнять «Синглтон-ориентированное программирование»: у вас есть один экземпляр каждого объекта и только один.

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

# 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 "$@"

Подсказки и советы

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

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

215
ответ дан Stefano Borini 23 November 2019 в 21:17
поделиться

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

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

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

24
ответ дан jtimberman 23 November 2019 в 21:17
поделиться

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

Кроме того общего принципа я собрал [приблизительно 110] общие ошибки сценария оболочки .

20
ответ дан pixelbeat 23 November 2019 в 21:17
поделиться

Была большая сессия в OSCON в этом году (2008) по просто этой теме: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf

13
ответ дан Jonathan Leffler 23 November 2019 в 21:17
поделиться

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

9
ответ дан 23 November 2019 в 21:17
поделиться

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

9
ответ дан user10392 23 November 2019 в 21:17
поделиться

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

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

10
ответ дан Paweł Hajdan 23 November 2019 в 21:17
поделиться

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

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

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

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

Для проверки сценарий является портативным, тест не только с #!/bin/sh, но также и использованием #!/bin/ash, #!/bin/dash, и т.д. Вы определите Bash определенный код достаточно скоро.

7
ответ дан Willem 23 November 2019 в 21:17
поделиться

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

"Жемчуг использования. Вы захотите знать удар, но не использовать его".

Печально я забыл, кто сказал это.

И да в эти дни я рекомендовал бы Python по жемчугу.

-1
ответ дан Sarien 23 November 2019 в 21:17
поделиться