Зачем писать целый bash-скрипт в функциях?

Зачем писать целый bash-скрипт в функциях?

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

#!/bin/bash

# Configure variables
declare_variables() {
    noun=geese
    count=three
}

# Announce something
i_am_foo() {
    echo "I am foo"
    sleep 0.5
    echo "hear me roar!"
}

# Tell a joke
walk_into_bar() {
    echo "So these ${count} ${noun} walk into a bar..."
}

# Emulate a pendulum clock for a bit
do_baz() {
    for i in {1..6}; do
        expr $i % 2 >/dev/null && echo "tick" || echo "tock"
        sleep 1
    done
}

# Establish run order
main() {
    declare_variables
    i_am_foo
    walk_into_bar
    do_baz
}

main

Есть ли какая-то причина делать это, кроме «читабельности», которую, я думаю, можно было бы так же хорошо обеспечить, добавив несколько дополнительных комментариев и добавив немного межстрочного интервала?

Делает ли это скрипт более эффективным (я бы ожидал обратного, если бы вообще что-то было), или упрощает ли это изменение кода за пределами вышеупомянутого потенциала читабельности? Или это действительно просто стилистическое предпочтение?

Обратите внимание, что хотя сценарий не демонстрирует это как следует, "порядок запуска" функций в наших реальных сценариях, как правило, очень линейный -- walk_into_barзависит от того, что i_am_fooбыло сделано, и do_bazдействует на то, что установлено walk_into_bar-- поэтому возможность произвольно менять порядок запуска -- это не то, что мы обычно делаем. Например, вы бы не захотели внезапно поставить declare_variablesпосле walk_into_bar, это бы все сломало.

Пример того, как я бы написал приведенный выше сценарий, был бы таким:

#!/bin/bash

# Configure variables
noun=geese
count=three

# Announce something
echo "I am foo"
sleep 0.5
echo "hear me roar!"

# Tell a joke
echo "So these ${count} ${noun} walk into a bar..."

# Emulate a pendulum clock for a bit
for i in {1..6}; do
    expr $i % 2 >/dev/null && echo "tick" || echo "tock"
    sleep 1
done

решение1

Удобочитаемостьэто одно. Но есть еще кое-чтомодуляризациячем просто это. (Полумодуляризация(Возможно, это более правильно для функций.)

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

Еще один плюс функций —возможность повторного использования. После кодирования функции ее можно применять в скрипте несколько раз. Вы также можете перенести ее в другой скрипт.

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

Еще один момент, который стоит добавить. Как замечает Etsitpab Nioliv в комментарии ниже, легко перенаправлять из функций как связной сущности. Но есть еще один аспект перенаправлений с функциями. А именно, перенаправления могут быть установлены вдоль определения функции. Например:

f () { echo something; } > log

Теперь не требуется явных перенаправлений при вызовах функций.

$ f

Это может избавить от множества повторений, что опять же повышает надежность и помогает поддерживать порядок.

Смотрите также

решение2

Я начал использовать этот же стиль программирования на bash после прочтенияЗапись в блоге Кфира Лави «Защитное программирование Bash». Он приводит довольно много веских причин, но лично я считаю эти наиболее важными:

  • Процедуры становятся описательными: гораздо проще понять, что должна делать конкретная часть кода. Вместо стены кода вы видите "О, функция find_log_errorsчитает этот файл журнала на предмет ошибок". Сравните это с поиском целой кучи строк awk/grep/sed, которые используют бог знает какой тип регулярных выражений в середине длинного скрипта — вы понятия не имеете, что он там делает, если нет комментариев.

  • вы можете отлаживать функции, заключая их в set -xи set +x. Как только вы убедитесь, что остальная часть кода работает нормально, вы можете использовать этот трюк, чтобы сосредоточиться на отладке только этой конкретной функции. Конечно, вы можете заключать части скрипта, но что делать, если это длинная часть? Проще сделать что-то вроде этого:

       set -x
       parse_process_list
       set +x
    
  • использование печати с cat <<- EOF . . . EOF. Я использовал его довольно много раз, чтобы сделать свой код намного более профессиональным. Кроме того, parse_args()с getoptsфункцией довольно удобно. Опять же, это помогает с читаемостью, вместо того, чтобы запихивать все в скрипт как гигантскую стену текста. Также удобно использовать их повторно.

И, очевидно, это гораздо более читабельно для того, кто знает C или Java, или Vala, но имеет ограниченный опыт работы с bash. Что касается эффективности, то тут не так много того, что вы можете сделать - bash сам по себе не самый эффективный язык, и люди предпочитают perl и python, когда дело касается скорости и эффективности. Однако вы можете использовать niceфункцию:

nice -10 resource_hungry_function

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

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

Вот некоторые примеры использования этого стиля:

решение3

В своем комментарии я упомянул три преимущества функций:

  1. Их легче тестировать и проверять правильность.

  2. Функции можно легко повторно использовать (извлекать) в будущих скриптах.

  3. Вашему боссу они нравятся.

И никогда не недооценивайте важность цифры 3.

Я хотел бы остановиться еще на одном вопросе:

... поэтому возможность произвольно менять порядок выполнения — это не то, что мы обычно делаем. Например, вы бы не захотели внезапно поставить declare_variablesпосле walk_into_bar, это бы все сломало.

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

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

решение4

Вы разбиваете код на функции по той же причине, по которой вы бы сделали это для кода C/C++, python, perl, ruby ​​или любого другого языка программирования. Более глубокая причина — абстракция: вы инкапсулируете низкоуровневые задачи в высокоуровневые примитивы (функции), так что вам не нужно беспокоиться о том, как все делается. В то же время код становится более читаемым (и поддерживаемым), а логика программы становится более понятной.

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

Связанный контент