SolitonNKは結果の表示だけじゃない。スクリプトを使って解析結果を基にアクションさせよう。（２／３）　anko スクリプトの書き方・使い方

SolitonNKでankoスクリプトを用いて、解析結果を基にアクションさせるための説明の2回目記事は、スクリプトの書き方です。

SolitonNKの anko モジュールは、検索パイプライン内で使える汎用スクリプトツールです。スクリプト作成は手間がかかりますが、検索エントリの非常に柔軟な操作ができるようになります。そしてスクリプトを作成したら、他のユーザーとも簡単に共有できます。

ankoスクリプト言語そのものについては、anko本家のドキュメントを参照していただくとして、本記事では SolitonNK で anko スクリプトをどのように使うかを簡単に説明します。

# ankoスクリプトの管理

検索でankoスクリプトを実行するには、スクリプトを含むテキストファイルをリソースとしてアップロードする必要があります。

リソースの作成や管理は、ユーザーインターフェイスのメインメニューの中にある「リソース/Resources」の画面でできます。新しいリソースを作成するには、右上の「追加/Add」ボタンを選択します。

<img width="393" alt="SolitonNK-リソースのアップロード.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480441/e7cdbed6-c85a-0499-e461-e85d2154e636.png">


新しいリソースの追加用画面では、リソース名と、（必要に応じて）説明を記述し（、そのリソースを読み取れるするグループを選択し）、アップロードするファイルを選択します。[保存/SAVE]ボタンを押すとリソースがアップロードして保存されます。

<img width="317" alt="SolitonNK-リソースのアップロード2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480441/63ebaa92-383f-74d3-039f-241e98e380a8.png">


スクリプト記述内容を変更するには、元のテキストファイルのスクリプトを編集してから、ファイルをリソースに再アップロードしてください。（今後、スクリプト作成を簡単にする統合テキストエディターが追加される予定です）

## ankoスクリプトを実行する

スクリプトを実行するには、検索実行欄で `anko`を指定し、その後にスクリプトの名前、スクリプト用の引数を続けます。たとえば、引数として2つの数値を取る `foo` という名前のスクリプトを実行するに パイプラインの記述の中で `anko foo 1 3` と記述すればよいです。

## ankoスクリプトを書く

ankoスクリプトは、`eval`コマンドと同じ構文を使用します。以下の例とevalモジュールのセクションを参照してください。

# 必須の関数定義

SolitonNKの検索欄から用いる ankoスクリプトにおいては、 `Process` 関数か、`Main` 関数かどちらかが必ず用いられなければなりません。これらの関数は引数をとりません。この2つの関数は、全く異なる検索エントリ処理方法の選択肢となります。

`Process`関数が定義されている場合、検索エントリごとに1回呼び出されます。エントリの列挙値はローカル変数として扱われ、`Process`関数の戻り値としては、エントリがパイプラインを続行できる場合には `true` 、できない場合には `false` が返されます。

`Main`関数が定義されている場合、呼び出しは一度だけです。したがって、`readEntry`関数や`writeEntry`関数を用いて各検索エントリの結果を取得し、パイプラインに渡すようにして、その後の処理ができるようにする必要があります。

可能な限り、`Main`関数よりも、概念的にシンプルな`Process`関数を使ってスクリプトを書くことが推奨されています。

# オプションの関数定義

`Parse`関数や`Finalize`関数をスクリプトに含めることができます。

`Parse`関数は、`Process`関数あるいは`Main`関数より前に呼び出され、引数の配列としてコマンドライン引数を与えることができます。`Parse`関数で、返り値として`nil`が返されると、引数が正常に処理されたことを表します。`nil`以外の戻り値はエラーとして扱われ、ユーザーに表示されます。スクリプトの引数を解析する方法のサンプルについては、以下のサンプルスクリプトを参照してください。

注意：Parse関数は必ず明示的な値を返り値として返します。返り値が`nil`の場合は解析の成功の通知です。それ以外値が返ってきた場合は、エラーを示します。エラーが発生した場合について、問題を説明する文字列を返すようにすることをお勧めします。

`Finalize`関数は、`Process`関数あるいは`Main`関数が完了した後の、スクリプトの最後に実行されるコードです。これは、リソースを作成する必要がある時に活用するのに適した場所です。

# Process（）関数を使用したサンプルスクリプト

このサンプルスクリプトは、スクリプトの引数指定によって2種類の動作モードが選択できます。'build'（作成）モードでは、`packet`モジュールから抽出された"SrcIP"フィールドを使用して、その時点での検索によって見つかった全てのIPアドレスのリストを作成し、そのリストをリソースとして保存します。 'apply'（適用）モードでは、以前の'build'モードの検索いよって作成されたテーブルを取得し、 'apply'モードの検索に用いられているエントリーから、"SrcIP"フィールドに以前に見つかったIPアドレスが記載されているエントリを全て削除します。
なお、このスクリプトは、ネットワーク上の新しいデバイスを探すために使用されてきましたが、現在は`lookup`モジュールにこのスクリプトと同じ機能を持っていて、より柔軟に使えます。

	table = make(map[string]interface)
	task = "build"

	var json = import("encoding/json")

	# first arg = "build" or "apply"
	func Parse(args) {
	    errstr = "argument must be 'build' or 'apply'"
	    if len(args) == 1 {
	        task = args[0]
	    } else {
	        return errstr
	    }
	    switch task {
	    case "apply":
	        # load the table
	        data, _ = getResource("lookuptable")
	        json.Unmarshal(data, &table)
	    case "build":
	    default:
	        return errstr
	    }
	    return nil
	}

	func Process() {
	    if task == "build" {
	        s = toString(SrcIP)
	        table[s] = true
	        return true
	    } else if task == "apply" {
	        s = toString(SrcIP)
	        # create & set an enumerated value named "new" to true or false
	        setEnum("new", !table[s])
	        # if it's not in the table, return true
	        return !table[s]
	    }
	}

	func Finalize() {
	    if task == "build" {
	        data, err = json.Marshal(table)
	        if err != nil {
	            return err
	        }
	        return setResource("lookuptable", data)
	    }
	}

"SrcIP"列挙値は、`Process`関数内の他の変数と同様に読み取れるように見えますが、列挙値を設定するためには`setEnum`関数を使用してアクションを明示する必要があります。

`table`および`task`変数は、関数定義の外部で宣言されていることに注意してください。組み込みのjsonエンコーディングライブラリも、関数定義の前にインポートされます。

# Main（）関数を使用したサンプルスクリプト

`Main`関数を使用してスクリプトを記述するのは、`Process`関数の場合より難しいですが、エントリを複製する必要がある場合は、`Main`関数を使わねばなりません。
次のスクリプトでは、`Modbus`メッセージを含むエントリを読み取ります。メッセージがタイプ`0x10`（「複数のレジスタを書き込む」）というリクエストの場合、スクリプトは書き込まれるレジスタごとに元のエントリを1回複製し、単一のレジスタアドレス＋レジスタ値を含む「RegAddr」および「RegValue」列挙値を各エントリにアタッチします。

注：このスクリプトは単独では機能しません。書かれているように、それはパイプラインのより上流での別のankoスクリプトの出力を利用することを目的としていたため、「Request」や「WriteAddr」などの列挙値が用いられています。

	func Main() {
	    for i = 0; i != -1; i++ {
	        ent, err = readEntry()
	        if err != nil {
	            return
	        }

	        # Check if this is a request or a response
	        Request, err = getEntryEnum(ent, "Request")
	        if err != nil {
	            #Request isn't set, this isn't a modbus packet, skip
	            continue
	        }

	        # read the function value
	        Function, err = getEntryEnum(ent, "Function")
	        if err != nil {
	            continue
	        }

	        ReqResp, err = getEntryEnum(ent, "ReqResp")
	        if err != nil {
	            continue
	        }

	        if Request == true {
	            if Function == 0x10 {
	                # write multiple registers
	                Addr, err = getEntryEnum(ent, "WriteAddr")
	                if err != nil {
	                    writeEntry(ent)
	                    continue
	                }
	                Count, err = getEntryEnum(ent, "WriteCount")
	                if err != nil {
	                    writeEntry(ent)
	                    continue
	                }
	                if Count == 0 || len(ReqResp) < 5 + 2*Count {
	                    writeEntry(ent)
	                    continue
	                }
	                for j = 0; j < Count; j++ {
	                    newEnt = cloneEntry(ent)
	                    # read the register value
	                    val = (toInt(ReqResp[5+(2*j)]) << 8) | toInt(ReqResp[5+(2*j)+1])
	                    setEntryEnum(newEnt, "RegAddr", Addr + j)
	                    setEntryEnum(newEnt, "RegValue", val)
	                    err = writeEntry(newEnt)
	                    if err != nil {
	                        continue
	                    }
	                }
	            } else {
	                writeEntry(ent)
	            }
	        }
	    }
	}

`Main`関数を用いたスクリプト記述では、`Process`関数では不要だった`readEntry`、`cloneEntry`、`writeEntry`といった関数によってエントリを明示的に管理・操作してることに注意してください。また、列挙型の値の読みだしには、`getEntryEnumand`関数や`setEntryEnum`関数を使用していて、変数の扱いとは異なっていることにも注意してください。



SolitonNKに用意されているanko スクリプトの書き方の説明はここまでです。
[次の記事](https://qiita.com/kakawai/items/141041a25f1b1b4b27bb)ではいよいよ、検索結果を基にウェブにアクションするサンプルスクリプトを紹介します。