Конечный автомат на bash
Думаю все из нас, кто учился на ИТ-специальностях, в университете изучали конечные автоматы. Для тех кто не в курсе, это абстрактный автомат способный находиться в конечном количестве состояний, переход из одного состояния в другое происходит при выполнение некоторых условий. Штука интересная, но не совсем понятно когда и как это можно применить для решения реальных задач. О том, как я пришел к решению возникшей задачи на основе конечного автомата, а также о том, как реализовал его на bash, я бы и хотел рассказать. А в качестве бонуса опишу как сохранять его состояние для возможности восстановить работу с прерванной точки.Задача была следующая: автоматизировать процесс импорта данных из внешних источников. Модули системы для импорта этих самых данных уже готовы, но в данный момент их приходится запускать вручную. Проблема в том, что импортировать объекты надо в определенной последовательности. В случаях, когда система не смогла автоматически связать полученные данные с уже существующими, надо отравлять операторам задание выполнить привязку вручную. И лишь после того, как они это сделали, можно продолжить импорт.Поскольку задача в основном состоит из запуска готовых приложений, решено было реализовать ее на bash.Для простоты вместо реально созданного скрипта здесь будет показан максимально упрощенный пример.
Первый вариант скрипта состоял из последовательного запуска необходимых команд и остановки в случае необходимости ручного вмешательства. Тут же возник вопрос о том, как продолжать роботу с прерванного места, чтобы не выполнять все операции с самого начала. Простейшее решение — место остановки записывать в текстовый файл, при старте считывать значение и переходить в нужное место. Но в bash нет оператора goto, с помощью которого можно было бы перейти к точке останова. Пришлось писать условия, а чтобы условия не выглядели вот так:
if [[ $STEP == 'step3' || $STEP == 'step4' || … || $STEP == 'step10' ]] на каждом шаге переменной $STEP присваивается значение следующего шага:
if [[ $STEP == 'step3' ]] then # Полезные действия. Возможно выход. STEP='step4' fi if [[ $STEP == 'step4' ]] then # Полезные действия. Возможно выход. STEP='step5' fi Теперь если в самом начале присвоить переменной STEP значение шага, мы сразу перейдем к нему, а далее выполнение скрипта будет последовательным.
Однако немного поразмыслив, я понял что ждать пока операторы выполнят свою часть работы не всегда нужно. Иногда можно отравить сообщение и продолжить работу пропустив один-два шага, а к ним вернуться позже. Скрипт начал приобретать вот такой вид:
if [[ $STEP == 'step3' ]] then # Полезные действия if [[ условие ]] then STEP='step4' else STEP='step5' fi Переписав условия несколько раз я понял, что пытаюсь создать конечный автомат. Что у скрипта есть некие состояние и при определенных условиях происходит переход из одного состояния в другое. Здесь следует уточнить, что состояние не обязательно должно быть статическим, это может быть и какой либо процесс, а момент перехода и условия определяются результатами его завершения.
Однако написанный код не позволял делать одну важную вещь — совершать произвольный переход. В нем последовательность переходов жестка зашита последовательностью шагов в коде. Шаги можно только пропускать. Осознав проблему код был переписан:
function step1 { # Действия выполняемые на шаге 1 }
function step2 { # Действия выполняемые на шаге 1 } …
while [[ -z $EXIT ]] do case $STEP in step1) step1 if [[ условие ]] then STEP='step2' else EXIT=1 fi ;; step2) step2 if [[ условие ]] then STEP='step3' else STEP='step1' fi ;; … step10) step10 if [[ условие ]] then STEP='step6' else STEP='step9' fi esac done В результате получили:
Полную изоляцию состояний. Каждое состояние — отдельная функция. Свободу в переходах между состояниями независимо от последовательности их описания в коде. Условия переходов я преднамеренно не стал помещать внутрь функций чтобы они представляли из себя состояния в чистом виде. Если вдруг условия станут слишком сложными, лучше создать для них отдельные функции переходов.Бонус: Сохранение состояния между вызовами. Поскольку мне была возможность остановить скрипт и продолжить его работу через некоторое время, я написал функцию сохранения состояния: function saveState { echo -e «STEP='$STEP'\n» > state } Вызов функции добавил в конец скрипта, а в начало загрузку состояния: source state.
Простой скрипт для экспериментов:
#!/bin/bash
STEP='step2' # Начальное состояние
function step1 { echo 'step 1' }
function step2 { echo 'step 2' }
# Сохранить состояние function saveState { # Таким образом можно сохранить значение любых переменных echo -e «STEP='$STEP'\n» > state; }
# Главный цикл function main { # Сигналом к завершению служит наличие значения у переменной EXIT while [[ -z $EXIT ]] do case »$STEP» in step1) step1 EXIT=1 ;; step2) step2 STEP='step1' ;; esac done }
# Если существует файл state прочитать сохраненные в нем значения (на самом деле выполнить) if [ -f state ] then source 'state' fi main # Главный цикл saveState # Перед завершением сохранить состояние. После первого вызова он выведет:
step2step1
После второго: test1
Сбросить состояние можно удалив файл state в директории скрипта.
Несколько советов Обратите внимание на то, что bash ОЧЕНЬ чувствителен к пробелам и их отсутствию. Также в нем необычное понимание числовых значений как истина и лож, 0 — истина (связано с тем, что программы возвращают 0 в случае успешного завершения, и отличное от нуля значение в случае ошибки). Для отладки скрипта полезно запускать его командой bash -x .