📜 ⬆️ ⬇️

Bash state machine

I think all of us who studied IT, at the university studied finite automata. For those who do not know, this is an abstract automaton capable of being in a finite number of states, the transition from one state to another occurs when certain conditions are met. The thing is interesting, but it is not entirely clear when and how this can be applied to solve real problems. I would like to tell you how I came to solving the arisen task on the basis of a finite state machine, as well as how I implemented it in bash. And as a bonus I will describe how to save its state for the ability to restore work from an interrupted point.

The task was the following: to automate the process of importing data from external sources. The system modules for importing these same data are already ready, but at the moment they have to be started manually. The problem is that you need to import objects in a certain sequence. In cases where the system could not automatically associate the data with existing data, it is necessary to send the task to operators to perform manual binding. And only after they have done this, can the import be continued.

Since the task mainly consists of running ready-made applications, it was decided to implement it in bash.
For simplicity, instead of the actually created script, the most simplified example will be shown here.

The first version of the script consisted of sequentially running the necessary commands and stopping if manual intervention was necessary. Immediately the question arose of how to continue the robot from an interrupted place in order not to carry out all operations from the very beginning. The simplest solution is to place a stop in a text file, read the value at start and go to the right place. But bash does not have a goto statement that could be used to go to a breakpoint. I had to write the conditions, and that the conditions did not look like this:
')
if [[ $STEP == 'step3' || $STEP == 'step4' || … || $STEP == 'step10' ]] 


At each step, the $ STEP variable is assigned the value of the next step:

 if [[ $STEP == 'step3' ]] then #  .  . STEP='step4' fi if [[ $STEP == 'step4' ]] then #  .  . STEP='step5' fi 


Now, if at the very beginning we assign the step value to the variable STEP, we will immediately move to it, and then the script will be executed sequentially.

However, after some thought, I realized that it was not always necessary to wait until the operators completed their part of the work. Sometimes you can poison a message and continue working by skipping one or two steps, and return to them later. The script began to take this form:

 if [[ $STEP == 'step3' ]] then #   if [[  ]] then STEP='step4' else STEP='step5' fi 


Rewriting the conditions several times, I realized that I was trying to create a state machine. That the script has some state and under certain conditions there is a transition from one state to another. It should be clarified here that the state does not have to be static, it can be any process, and the moment of transition and conditions are determined by the results of its completion.

However, the written code did not allow to do one important thing - to make an arbitrary transition. In it the sequence of transitions is rigidly sewn up with a sequence of steps in the code. Steps can only be skipped. Realizing the problem, the code was rewritten:

 function step1 { #     1 } function step2 { #     2 } … 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 


As a result, received:

I deliberately did not place the conditions for transitions inside functions so that they are pure states. If conditions suddenly become too complicated, it is better to create separate transition functions for them.

Bonus: Preservation of state between calls.

Since I needed the ability to stop the script and continue its work after a while, I wrote a state saving function:

 function saveState { echo -e «STEP='$STEP'\n» > state } 


The function call was added to the end of the script, and to the beginning of the loading state: source state.

A simple script for experiments:

 #!/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 #    . 


After the first call, it will output:
step2
step1

After the second:
step1


You can reset the state by deleting the state file in the script directory.

Some tips

Please note that bash is VERY sensitive to spaces and their absence. It also has an unusual understanding of numeric values ​​as true and false, 0 is true (due to the fact that programs return 0 if successful, and a nonzero value in case of error). To debug a script, it is useful to run it with the bash -x <script> command.

Update May 25, 2015


Having come out on Monday to the robot and after a little more thought, I realized that the code can be improved a little more by getting rid of the case. Also taken into account are the kt97679 tips on using trap, and ZyXI on printf.

 #!/bin/bash STEP='step2' #   #   1 function step1 { echo 'step 1' } #    1 function step1Next { exit } #   2 function step2 { echo 'step 2' } #    2 function step2Next { STEP='step1' } #   function saveState { #       ,      >> printf "STEP=%q\n" "$STEP" > state } #   function main { while true do $STEP $STEP'Next' done } function loadState { #    state      (   ) if [ -f state ] then source 'state' fi } trap saveState EXIT #     loadState #    main #   


Now you can add any number of states without changing the main loop.
For state names, I advise you to use some kind of prefix so that it is unambiguously clear that this is a state function.

Source: https://habr.com/ru/post/258605/


All Articles