Шаблони дизайну або кращі практики для сценаріїв оболонок [закрито]


167

Хтось знає про будь-які ресурси, які говорять про кращі практики чи схеми дизайну для скриптів оболонки (sh, bash тощо)?


2
Я щойно написав статтю про шаблон шаблону в BASH минулої ночі. Подивіться, що ви думаєте.
скоромовка

Відповіді:


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 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 із наступним синтаксисом

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

Який буде шуканий у SHELL_LIBRARY_PATH. Коли ви завжди імпортуєте у глобальну область імен, не забудьте встановити всі свої функції та змінні з відповідним префіксом, інакше ви ризикуєте зіткненням імен. Я використовую подвійне підкреслення як крапку пітона.

Крім того, поставте це як перше, що у вашому модулі

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

Об'єктно-орієнтоване програмування

В bash, ви не можете робити об'єктно-орієнтоване програмування, якщо тільки ви не побудуєте досить складну систему розподілу об'єктів (я думав про це. Це можливо, але божевільно). На практиці, однак, ви можете робити "програмування, орієнтоване на одиночку": у вас є один екземпляр кожного об'єкта і лише один.

Що я роблю: я визначаю об'єкт у модулі (див. Запис модуляризації). Тоді я визначаю порожні 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 "$@"

Підказки та поради

Якщо з якихось причин щось не працює, спробуйте змінити порядок коду. Порядок важливий і не завжди інтуїтивний.

навіть не розглядайте роботу з tcsh. він не підтримує функції, і це взагалі жахливо.

Сподіваюся, це допоможе, хоча зверніть увагу. Якщо вам доведеться використовувати такі речі, про які я писав тут, це означає, що ваша проблема занадто складна, щоб її вирішити з оболонкою. використовувати іншу мову. Мені довелося використовувати його через людські чинники та спадщину.


7
Нічого собі, і я думав, що збираюся зайвий збиток в баші ... Я схильний використовувати ізольовані функції і зловживати підшлубками (таким чином, я страждаю, коли швидкість у будь-якому разі актуальна). Ніяких глобальних змінних ніколи, ні в і не (щоб зберегти залишки розуму). Все повертається через stdout або вихідний файл. set -u / set -e (занадто поганий набір -e стає марним, як тільки спочатку, і більшість мого коду часто є там). Аргументи функції, взяті з [local something = "$ 1"; shift] (дозволяє легко переупорядкувати під час рефакторингу). Після одного сценарію на 3000 рядків, я схильний писати навіть найменші сценарії таким чином ...
Євген

невеликі виправлення для модуляризації: 1 вам потрібно повернутися після. "$ script_absolute_dir / $ module.shinc", щоб уникнути пропуску попередження. 2 перед встановленням модуля пошуку в $ SHELL_LIBRARY_PATH
Duff

"людські фактори" - найгірші. Машини не борються з вами, коли ви даєте їм щось краще.
jeremyjjbrown

1
Чому getoptvs getopts? getoptsє більш портативним і працює в будь-якій оболонці POSIX. Тим більше, що питання полягає в найкращій роботі з оболонками, а не в конкретних обставинах, я б підтримав відповідність POSIX для підтримки кількох оболонок, коли це можливо.
Wimateeka

1
дякую, що ви запропонували всі поради щодо сценаріїв оболонок, навіть якщо ви чесно: "Сподіваюся, це допомагає, хоча зверніть увагу. Якщо вам доведеться використовувати такі речі, про які я писав тут, це означає, що ваша проблема занадто складна, щоб вирішити її оболонку. використовуйте іншу мову. Мені довелося користуватися нею через людські фактори та спадщину ".
dieHellste

25

Погляньте на Розширений посібник з сценаріїв Bash, щоб отримати багато мудрості в сценаріях оболонок - і не тільки Bash.

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

Як зазначалося, існує не так багато "найкращих практик" чи "моделей дизайну" для сценаріїв оболонок. Різне використання має різні рекомендації та упередження - як і будь-яка інша мова програмування.


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

20

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

Окрім цього загального принципу, я зібрав деякі поширені помилки сценарію оболонки .



11

Знайте, коли ним користуватися. Для швидкого та брудного склеювання команд це добре. Якщо вам потрібно прийняти більше ніж кілька нетривіальних рішень, циклів і нічого, перейдіть на Python, Perl та модулюйте .

Найбільша проблема з оболонкою часто полягає в тому, що кінцевий результат просто схожий на велику кульку грязі, 4000 ліній і тріщину ... і ви не можете її позбутися, тому що тепер від цього залежить весь ваш проект. Звичайно, він стартував з 40 ліній красивого баша.


9

Легко: використовуйте python замість оболонок скриптів. Ви отримуєте читабельність майже в 100 разів, не ускладнюючи нічого, що вам не потрібно, і зберігаючи здатність еволюціонувати частини вашого сценарію у функції, об'єкти, стійкі об’єкти (zodb), розподілені об'єкти (pyro) майже без жодного додатковий код.


7
ви суперечите собі, говорячи "без необхідності ускладнювати", а потім перераховуєте різні складності, які, на вашу думку, додають цінності, а в більшості випадків зловживають некрасивими монстрами, а не використовуються для спрощення проблем та реалізації.
Євген

3
це означає великий недолік, ваші сценарії не будуть портативними в системах, де python немає
астропанічний

1
Я усвідомлюю, що на це відповіли '08 (це два дні до '12); однак, для тих, хто дивиться на це років пізніше, я б застеріг когось від того, щоб не повертатися спиною на такі мови, як Python або Ruby, оскільки більш імовірно, що вони доступні, а якщо ні, то це команда (або кілька клацань) від встановлення. . Якщо вам потрібна додаткова портативність, подумайте про те, щоб написати свою програму на Java, оскільки ви будете важко натискати, щоб знайти машину, у якій немає JVM.
Віл Мур III

@astropanic майже всі порти Linux з Python сьогодні
Pithikos

@ Pithikos, звичайно, і поспілкуйся із клопотами python2 vs python3. Сьогодні я пишу всі свої інструменти за допомогою go, і не можу бути щасливішою.
астропанічний

9

використовувати set -e, щоб ви не плугали вперед після помилок. Спробуйте зробити його сумісним, не покладаючись на bash, якщо ви хочете, щоб він працював на не-linux.


7

Щоб знайти деякі "найкращі практики", подивіться, як дистрибутив Linux (наприклад, Debian) пише свої init-скрипти (зазвичай їх можна знайти в /etc/init.d)

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

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

Я намагаюся уникати функцій, оскільки вони, як правило, ускладнюють сценарій. (Perl був створений для цієї мети.)

Щоб переконатися, що сценарій є портативним, протестуйте не тільки #! / Bin / sh, але також використовуйте #! / Bin / ash, #! / Bin / dash тощо. Ви досить швидко помітите специфічний код Bash.


-1

Або старша цитата, схожа на те, що сказав Жоао:

"Використовуйте perl. Ви хочете знати bash, але не використовувати його."

На жаль, я забув, хто це сказав.

І так, сьогодні я б рекомендував python over perl.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.