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

167

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

user14437
источник
2
Я только что написал небольшую статью о шаблонах в BASH прошлой ночью. Посмотрите, что вы думаете.
quickshiftin

Ответы:

222

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

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

мольба

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

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

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

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

Стефано Борини
источник
7
Ух ты, и я подумал, что собираюсь излишним в bash ... Я склонен использовать изолированные функции и злоупотреблять подоболочками (поэтому я страдаю, когда скорость имеет какое-либо значение). Никаких глобальных переменных никогда не было ни внутри, ни снаружи (чтобы сохранить остатки здравомыслия). Все возвращается через стандартный вывод или файл вывода. set -u / set -e (слишком плохо set -e становится бесполезным, как только первый, и большая часть моего кода часто там). Аргументы функции взяты с [local нечто = "$ 1"; shift] (позволяет легко изменить порядок при рефакторинге). После одного сценария bash на 3000 строк я склонен писать даже самые маленькие сценарии таким образом ...
Евгений
Небольшие исправления для модульности: 1 вам нужен возврат после. «$ script_absolute_dir / $ module.shinc», чтобы избежать пропущенного предупреждения. 2 вы должны установить IFS = "$ сохраненный_IFS" до вашего возвращения в модуль поиска в $ SHELL_LIBRARY_PATH
Дафф
«человеческие факторы» являются худшими. Машины не борются с вами, когда вы даете им что-то лучше.
jeremyjjbrown
1
Почему getoptпротив getopts? getoptsявляется более переносимым и работает в любой оболочке POSIX. Тем более что вопрос состоит в том, чтобы использовать лучшие практики оболочки, а не только лучшие практики bash, я бы поддерживал соответствие POSIX для поддержки нескольких оболочек, когда это возможно.
Wimateeka
1
спасибо за предложение всех советов по написанию сценариев оболочки, даже если вы честны: «Надеюсь, это поможет, хотя, пожалуйста, обратите внимание. Если вам приходится использовать то, что я здесь написал, это означает, что ваша проблема слишком сложна, чтобы ее можно было решить shell. используй другой язык. Я должен был использовать его из-за человеческих факторов и наследия. "
dieHellste
25

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

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

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

jtimberman
источник
9
Обратите внимание, что для сценариев даже небольшой сложности это НЕ лучший метод. Кодирование - это не просто заставить что-то работать. Речь идет о быстром, простом построении, надежности, многократном использовании, удобстве чтения и обслуживания (особенно для других). Сценарии оболочки плохо масштабируются до любого уровня. Более надежные языки намного проще для проектов с любой логикой.
дрифтер
20

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

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

pixelbeat
источник
11

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

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

Павел Хайдан
источник
9

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


источник
7
Вы противоречите себе, говоря «без необходимости усложнять», а затем перечисляете различные сложности, которые, по вашему мнению, увеличивают ценность, в то время как в большинстве случаев их используют в уродливых монстрах, а не для упрощения проблем и реализации.
Евгений
3
это подразумевает большой недостаток: ваши сценарии не будут переносимы в системах, где отсутствует python
astropanic
1
Я понимаю, что на это ответили в '08 (сейчас два дня до '12); Тем не менее, для тех, кто смотрит на это спустя годы, я бы предостерег кого-либо от того, чтобы отвернуться от таких языков, как Python или Ruby, так как более вероятно, что он доступен, а если нет, то это команда (или пара щелчков) от установки. , Если вам нужна дополнительная переносимость, подумайте о написании своей программы на Java, так как вам будет сложно найти машину, на которой нет JVM.
Уил Мур III
@astropanic почти все порты Linux с Python в настоящее время
Pithikos
@Pithikos, конечно, и возиться с хлопотами python2 против python3. В настоящее время я пишу все свои инструменты с го, и не могу быть счастливее.
астропаник
9

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

user10392
источник
7

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

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

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

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

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

Willem
источник
-1

Или более старая цитата, похожая на ту, что сказал Хуан:

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

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

И да, я бы порекомендовал Python поверх Perl.

Sarien
источник