令和の時代にAWK入門　AWKでマインスイーパーを実装

# はじめに

AWKはテキスト処理に特化した言語として古くから親しまれています。

テキスト処理には、普遍的な部分も大きく、シェル芸やログの抽出等でお世話になっています。

プログラミング言語としても結構協力で[HTTPサーバーを実装している](https://qiita.com/Syuparn/items/09205af50bcead3c95b0)例なども！

しかし、それでプログラムを書こうという人はこのご時世多くはないようです。

少しでも多くの方にその魅力を知っていただけるようネタとして、解説・サンプルコードを掲載します。、

AWKの奥深い魅力に足を踏み入れるきっかけになれば幸いです。

~~ぶっちゃけこんな暇なこと誰もやらねーだろうなと、休日の暇つぶしに一つ~~





# 基本的な使い方

基本的に下記のようにな構成で、成り立っている。

パターン・BEGIN・ENDは省略可能。

    awk '/パターン/BEGIN{# begin process  }{# main }END{# end}'

- BEGINブロック

初期化や設定のために使用されます。

変数の初期化や定数の設定などに用いられることが一般的です。

- ENDブロック

ENDブロックは

処理が終わったあと実行されます。

一般的な用途としては、最終結果の出力や集計、レポートの生成、統計情報の計算などがあります。

# ありがち？な使い方

つたないながら例として、`kubectl`の出力を加工して利用することをあげます。

ご存じたとは思いますが、このような出力が得られます。

~~~bash
$ kubectl  get pod
NAME                                    READY   STATUS    RESTARTS      AGE
my-release-mariadb-0                    1/1     Running   5 (15d ago)   43d
...etc...
~~~

例えば、get pod した結果から、"eth"を含む行を抽出し、printfで成形した結果を出す。

~~~bash
$ kubectl  get pod | awk '/eth/{printf("pod %s is in %s %s state\n",$1,$2,$3)}'
pod mysoftether-0 is in 1/1 Running state
~~~

`$0,$1,$2...`というのは特別な変数でそれぞれ、行の全文、第一フィールド、第二フィールドと格納されていきます。



例えば、pod内のすべてのコンテナが起動していることを確認する。

第二フィールドを切り出して、"/"で分割、割った値が1でなければおかしいよ！って判定しています。

~~~bash
$ kubectl get pod | awk '{
>     if( split($2,foo,"/") == 2){
>         res=foo[1]/foo[2]
>         if( res == 1 && $3 == "Running" ){
>             print $1 " status ok"
>             okpod++
>         }else{
>             print $1 "status no ok"
>         }
>     }
> }
> END{
>     print okpod " pod ready"
> }'
my-release-mariadb-0 status ok
my-release-wordpress-6c944fcd7c-jskqw status ok
mydeployment-75c9cc4f7b-xxnbn status ok
mydnsmasq-6d496c6469-mp9x6 status ok
mysoftether-0 status ok
5 pod ready
~~~


# 言語仕様みたいなところ


## 変数の型と扱い

AWK言語では、変数の型宣言は必要ありません。変数は最初に代入された値によって型が自動的に決定されます。代入される値によって数値型または文字列型として解釈されます。

変数の扱いにおいては、AWK言語では数値と文字列の間で自動的な型変換が行われます。また、変数の初期化も明示的に行う必要はありません。

また、連想配列はありますが、構造体はありません。

連想配列をネストするようなこともできませんでした。


## 引数の扱い

AWK関数では、引数を指定することができます。関数の引数は、関数名の後にカッコ内に引数名を記述して定義します。AWK言語では、引数は暗黙的な型宣言が行われず、関数内で使用する際には型の変換が自動的に行われます。

引数の扱いにおいては、スカラーか否かで挙動が変わります。スカラーにおいては、値渡しが行われ、配列は参照渡しとなるようです。

## 乱数について

`rand()`関数により生成できます。

しかし、 `srand()`を呼びシードを設定しないと、同じ値を返し続けてしまうようです。

また、`srand()`により、現在時刻（秒単位）がシードとして設定されるため、1秒以内に、複数回の呼び出しを行った場合、同じ値が帰るようです。

## 参考文献

https://qiita.com/theworld/items/ed771bd727beaf110aaf

https://docs.oracle.com/cd/F16635_01/html/E71065/awk-1.html

https://qiita.com/neko_the_shadow/items/f4f3d339d1919240b6ee



# いざ実装

このプログラムでは、しかるべき入力がないため、BEGINブロック内で実行しています。

また、関数や変数は、実行時点で存在していることが必要です。

ありがちなロジックであるため、説明はほとんどしませんが、「AWKでこういう書き方ができる」とか「文法のエッセンス」みたいなものを感じていただければ幸いです。（気が向いたら解説か来ます）

得意でも書きなれているわけでもないので、エトセトラおかしな点はあると思いますが…


~~~awk
function Init(und){
        # マインスイーパーの盤面を初期化します。
        # und: 盤面の2次元配列

        B=1;                # 省略可能ですが、行末に;を書きます。
        srand()             # 乱数シードを設定
        for (;B <= BOM;){   # 条件を省略したfor文

                x=int(rand() * TABLE_S) # キャスト
                y=int(rand() * TABLE_S)

                if( (x > 0 && x <= TABLE_S) && (y > 0 && y <= TABLE_S) ){
                        if(und[x][y] >= 0){
                                printf("set bom x: %d, y: %d\n",x,y)
                                und[x][y]=-9
                                B++
                                
                                # 八近傍に数値を加算する(for)
                                for(yy=y-1; yy<=y+1; yy++){
                                        for(xx=x-1; xx<=x+1; xx++){
                                                if(xx != x || yy != y){
                                                        und[xx][yy]++
                                                }
                                        }
                                }

                        }
                }

        }
}

function ReplaceCal(un){
        # 盤面の状態を表した数値を適当な文字に置き換えます。
        # un: 盤面の任意の一文字

        if(un < 10) return "#"  # {}の省略が可能です

        if(un < 20) return "B"

        if(un%20 != 0) return un%20

        if(un%20 == 0) return " "

        return "error"
}

function Show(und,all){
        # 盤面を描写します
        # und: 盤面の2次元配列
        # all: すべてのマスを開けて表示するか否か
        
        openflag=0
        printf("   ")

        for(i=1; i <= TABLE_S; i++){
                printf("%d ",i)
        }

        print ""

        if(all == TRUE) openflag=OPENFLAG

        for(y=1; y <= TABLE_S; y++){
                printf("%02d ",y)
                for(x=1; x <= TABLE_S; x++){
                        printf("%s ", ReplaceCal(und[x][y]+openflag))
                }
                print ""
        }
}

function OpenTable(und,x,y){
        # 空白マスであれば、指定されたマスを開け、再帰的に周辺ますを探索します。
        # und: 盤面の2次元配列
        # x,y: 盤面の座標
        
        if( (x > 0 && x <= TABLE_S) && (y > 0 && y <= TABLE_S) ){
                und[x][y]+=OPENFLAG
                if(und[x][y] == OPENFLAG){
                        OpenTable(und,x+1,y+1)
                        OpenTable(und,x+1,y)
                        OpenTable(und,x+1,y-1)
                        OpenTable(und,x,y+1)
                        OpenTable(und,x,y-1)
                        OpenTable(und,x-1,y+1)
                        OpenTable(und,x-1,y)
                        OpenTable(und,x-1,y-1)
                }

        }

        return
}

function Judge(und){
        # 盤面の状態を評価します
        # 盤面の2次元配列
        
        flag="Continue"
        cnt=0

        for(y=1;y <= TABLE_S; y++){
                for(x=1; x <= TABLE_S; x++){
                        if(und[x][y] > 10 && und[x][y] < OPENFLAG){
                                return "Lose"
                        }
                        if(und[x][y] >= OPENFLAG){
                                cnt++
                        }
                }
        }
        if(cnt == (TABLE_S*TABLE_S-BOM) ){
                flag="Win"
        }
        return flag
}

# パイプやファイルからの入力の前に実行されます
# このプログラムの場合、入力がないので、メイン処理の実行のために使います。
BEGIN{

# とりうる値
# 空いているマスは、20*nが加算される
# 逐次盤面を評価していること、数値を文字に置き換える関数がトリック

# BOM -9 ~ -1
# 空白マス 0
# 近傍 1 ~ 8

TABLE_S=10
BOM=10
OPENFLAG=20
TRUE="True"
FALSE="False"

tableUnd[TABLE_S+2][TABLE_S+2]=0

Init(tableUnd)
Show(tableUnd,TRUE)
print ""
Show(tableUnd,FALSE)
print ""

# 無限ループ（while）
while(1) {
        printf("input x, y :>")
        getline res < "-" # 標準入力からの入力を受け付ける
        #print res
        #print split(res,xy," ") # " "で分割
        if(split(res,xy," ") == 2){
                x=int(xy[1])
                y=int(xy[2])
                print x, y
                if(x > 0 && x <= TABLE_S && y > 0 && y <= TABLE_S){
                        OpenTable(tableUnd,x,y)
                        Show(tableUnd,FALSE)
                        Judge(tableUnd)
                }
        }
        if(Judge(tableUnd) != "Continue"){
                print Judge(tableUnd)
                exit
        }
}

}
~~~

# もちろん遊べます！
上記コードを適当な名前で保存

~~~bash
$ awk -f minesweeper.awk
set bom x: 4, y: 3
set bom x: 4, y: 2
set bom x: 2, y: 5
set bom x: 1, y: 3
set bom x: 2, y: 2
set bom x: 5, y: 5
set bom x: 3, y: 4
set bom x: 9, y: 7
set bom x: 8, y: 9
set bom x: 8, y: 8
   1 2 3 4 5 6 7 8 9 10
01 1 1 2 1 1
02 2 B 3 B 2
03 B 3 4 B 2
04 2 3 B 3 2 1
05 1 B 2 2 B 1
06 1 1 1 1 1 1   1 1 1
07             1 2 B 1
08             2 B 3 1
09             2 B 2
10             1 1 1

   1 2 3 4 5 6 7 8 9 10
01 # # # # # # # # # #
02 # # # # # # # # # #
03 # # # # # # # # # #
04 # # # # # # # # # #
05 # # # # # # # # # #
06 # # # # # # # # # #
07 # # # # # # # # # #
08 # # # # # # # # # #
09 # # # # # # # # # #
10 # # # # # # # # # #

input x, y :>8 2
8 2
   1 2 3 4 5 6 7 8 9 10
01 # # # # 1
02 # # # # 2
03 # # # # 2
04 # # # # 2 1
05 # # # # # 1
06 1 1 1 1 1 1   1 1 1
07             1 2 # #
08             2 # # #
09             2 # # #
10             1 # # #
input x, y :>
~~~