bashのステートマシン

ITの専門分野を学んだ私たち全員が、大学で有限状態マシンを研究したと思います。 知識のない人にとって、これは有限の状態にあることができる抽象的なオートマトンであり、ある状態から別の状態への遷移は、特定の条件が満たされたときに発生します。 興味深いことですが、実際の問題を解決するためにいつ、どのように適用できるかは完全には明らかではありません。 有限状態マシンに基づいて発生した問題をどのように解決したか、またそれを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変数にステップ値を割り当てた場合、すぐにその変数に移動し、スクリプトが順番に実行されます。



しかし、少し振り返ってみると、オペレーターが作業の一部を完了するまで待つ必要は必ずしもないことがわかりました。 場合によっては、メッセージをポイズニングして、1つまたは2つの手順をスキップして作業を続行し、後でそれらに戻ることができます。 スクリプトは次の形式を取り始めました。



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







条件を数回書き換えて、ステートマシンを作成しようとしていることに気付きました。 スクリプトに何らかの状態があり、特定の条件下で、ある状態から別の状態に移行すること。 ここで、状態は静的である必要はなく、任意のプロセスである可能性があり、遷移の瞬間と条件は、その完了の結果によって決定されることを明確にする必要があります。



ただし、記述されたコードでは、1つの重要なことを行うことができませんでした-任意の遷移を行うためです。 その中で、遷移のシーケンスは、コード内の一連のステップによって固定されています。 スキップできるのは手順のみです。 問題を認識して、コードを書き直しました。



 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
      
      







その結果、次のものを受け取りました。



遷移条件を関数内に意図的に配置せず、純粋な状態にしました。 条件が突然複雑になりすぎた場合は、条件ごとに個別の遷移関数を作成することをお勧めします。



ボーナス:呼び出し間で状態を保存します。


スクリプトを停止してしばらくしてから作業を続行する機能が必要だったため、状態を保存する関数を作成しました。



 function saveState { echo -e «STEP='$STEP'\n» > 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 #    .
      
      







最初の呼び出しの後、次を出力します。

ステップ2

step1



2回目以降:

step1





スクリプトディレクトリの状態ファイルを削除することにより、状態をリセットできます。



いくつかのヒント


bashはスペースとその不在に非常に敏感であることに注意してください。 また、数値が真と偽、0-真(プログラムが成功すると0を返し、エラーの場合はゼロ以外の値を返すため)として数値を異常に理解します。 スクリプトをデバッグするには、コマンドbash -x <script>を使用して実行すると便利です。



2015年5月25日パッチ



月曜日にロボットに行ってもう少し考えて、ケースを取り除くことでコードがもう少し改善できることに気付きました。 また、トラップの使用に関するkt97679のヒント、およびprintfでのZyXIも含まれています。



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







これで、メインループを変更せずに状態をいくつでも追加できます。

状態名については、これが状態関数であることを明確に理解できるように、何らかのプレフィックスを使用することをお勧めします。



All Articles