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

StackOverflow https://stackoverflow.com/questions/78497

  •  09-06-2019
  •  | 
  •  

Вопрос

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

Это было полезно?

Решение

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

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

Призыв

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

импортировать "AModule/ModuleFile"

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

Кроме того, поместите это первым делом в свой модуль.

# 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.он не поддерживает функции, и это вообще ужас.

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

Другие советы

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

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

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

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

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

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

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

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

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

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

Чтобы найти некоторые «лучшие практики», посмотрите, как работают дистрибутивы Linux (например.Debian) пишут свои сценарии инициализации (обычно находятся в /etc/init.d)

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

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

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

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

Или старая цитата, похожая на то, что сказал Жоао:

«Используйте Perl.Вы захотите знать bash, но не будете его использовать».

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

И да, в наши дни я бы рекомендовал Python вместо Perl.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top