2019年11月6日 | ブログ記事

AWK「グフフ……」女騎士「くッ……!」[アドベントカレンダー2019 7日目]

Fourmsushi

この記事は traPアドベントカレンダー2019 の記事です。


ウィドンズ平原の西、ユニスク大森林の洞窟に、女の怒声が響いた。

「やめろっ!!こんなところに連れてきて私に何をさせるつもりだっ!!!」

彼女の名はローラ。聖シェルクス王国の騎士である。

もとは小さな村の出であったが、王都で行われる剣術大会にてその実力を認められ騎士となった、優れた実力の持ち主である。

平民である彼女を妬む者の策により、単騎でのオーク討伐を命じられ今に至る。

AWK「お前にはここで、AWKを習得してもらう」

ローラ「え……?」

ローラはPython流剣術の使いであったが、それ以外の剣術については全く知らないのであった。

……

みなさんこんばんは。18のFourmsushiです。
今回はプログラミング言語「AWK」についてお話しします。

最初の茶番はやってみたかっただけです。ゆるして、女騎士要素はありません。

AWKとは?

AWKは、Alfred V. Aho、 Peter J. Weinberger、 and Brian W. Kernighanらの三人によって開発されたプログラミング言語とその派生のことを指します。

現在使われている実装には3種類あり、macOSをはじめとしたBSD系のシステムではnawkが、多くのLinuxディストリビューションではgawk、また一部のディストリビューションではmawkが使われています。

主にAWKはテキスト処理に使われる言語で、他の多くのプログラミング言語とは異なり「データ指向」と呼ばれることがあります。
現代でも、コマンドラインでsedcutなどの補助としてしばしば用いられています。

この記事では、そんなAWKのごく基本的な機能と、ちょっと変わった使い方について説明します。
gawkを前提として説明をしますが、多くの内容はほかのAWKを使っていても問題なく実行できるはずです。

導入

Windows

WSLを使う、またはGitBashに付属のものを使うのが簡単で良いと思います。

macOS

前提:brew

$ brew install gawk

その他

自分で調べてください><

テキストエディタ

どんなものを使っても問題ないですが、私はVSCodeにawk-language-clientという拡張を入れて書いています。
AWKのLSPを使っており、機能が多くて便利です。

また、実用にあたってはテキストエディタでAWKを書くことは少ないのではないでしょうか。

Hello, World!

ハロワの元ネタはKernighanらしいですね。

プログラミング言語を学ぶにあたって、多くの人が最初に実行するのはHello, World!のプログラムです。
一見すると単純すぎて無意味に思えますが、標準出力の方法、コードの書き方、実行方法を一度に学ぶことができる良い題材です。

AWKのそれは以下のように書きます。

シェル上でハロワ

awk 'BEGIN { print "Hello, World!" }'

PythonやRubyなどのスクリプト言語が存在する今、AWKで長いコードを書くことは少なくなっています。
したがって、コマンドラインで用いるこちらの方法がより実用的だと言えるでしょう。

.awkファイルを書く

#! /bin/awk -f
BEGIN { print "Hello, World!! }

awk -f hello.awk または chmod +x hello.awk && ./hello.awk

先ほどのものとほとんど同じですが、ファイルからコードを実行する際には-fオプションをつけなければいけません。
また、shebangを書くことで、シェルスクリプトと同じようなかたちで実行することもできます。

解説

一度なにかしらのプログラミング言語に触れたことのある方にとって、print "hoge"の部分は説明不要でしょう。
ですが、printは関数ではないということに注意が必要かもしれません。

BEGIN {}は、後述する「パターンとアクション」のひとつです。この波括弧の中に書かれた処理は、プログラムの最初に実行されます。

パターンとアクション

基本

AWKのプログラムを構成するなかで最も重要なもののうちのひとつがこの「パターンとアクションです。」
AWKは、入力行がパターンにあてはまったときにそれに対応するアクションを実行します。
パターンとアクションは以下のような文法で書かれます。

pattern {
    action1;
    action2;
}

改行やセミコロンはは必須ではありませんが、複数のことをやりたいのであれば書くべきです。

フィールド

入力の一部のみをパターンとのマッチングに使用したり、入力の一部のみを出力したいということも多くあると思います。
AWKは入力をFS(Field Separator)で区切っているので、それを利用すれば入力の一部を取り出すことができます。
デフォルトのFSは半角スペースです。

フィールド$0は入力行を、$1以降はそれぞれのフィールドを指します。

例えば、1 aという入力の場合$01 a$11$2aです。
以下のコマンドで確かめてみましょう。

echo "1 a" | awk '{ print $0; print $1; print $2;}'

パターンの種類

パターンとして使えるものは以下のとおりです。

(例で使われている、引数のないprintprint $0と同じ出力をします。)

実際に試す

習うより慣れろ。
ls -alの出力の中から、ディレクトリの行だけを出力してみましょう。
AWKに入力を渡すにはパイプラインを使います。

解答
ls -al | awk '$1 ~ /^d/ { print }'

できましたか?

変数

他のプログラミング言語と同様に、AWKも変数を使うことができます。変数の宣言、代入はhennsuu = 12のようにして行います。
型については忘れてください。

文字列型の変数は四則演算において、0として扱われます。

BEGIN {
    print ("string" + 1); # 1
    print ("string" * 20); # 0
    print (20 / "string"); # 「致命的: ゼロによる除算が試みられました」とエラーが出ます
}

組み込み変数

変数について説明した以上は、組み込み変数についても説明する必要があります。

AWKには多くの組み込み変数があります。
ここではその全てを説明することはしませんが、代表的・重要なものを以下に示します。

実際に試す

ls -alの出力が何行あるかAWKをつかって確かめてみましょう。

解答
ls -al | awk 'END { print NR }'

制御構文

C++やらJavaScriptのような言語を普段書いている人は「forとかifはないの?」と思っているかもしれません。
もちろんありますのでご心配なく。

わたしの体力の都合上、簡単なものは例示のみに留めさせていただきます。
実行結果がわからないものは、ぜひ手元で実行してみてください。

if-else

BEGIN {
    a = 12;
    if (a > 12) {
        print "a > 12";
    } else {
        print "(☝ ՞ਊ ՞)☝";
    }
}

while/do-while

BEGIN {
    while (1) { print "^^"; }
}
BEGIN {
    do {
        print "do!";
    } while (1)
}

for, break, continue

BEGIN {
    for(i = 0; i < 10; i++) {
        print i;
        if ( i == 6 ) {
            break;
        } 
    }
}

または後述する配列を用いて

BEGIN {
    a[0] = "neko";
    a[1] = "nya";
    a[2] = "buri";

    for (b in a) {
        if (b == 2) {
            continue;
        }
        print a[b];
    }
}

switch-case

BEGIN {
    for (b = 0; b < 10; b++) {
        switch (b) {
            case 2:
                print "2.........................";
                break;
            case 4:
                print "4だが?";
                break;
            default:
                print "^^";
                break;
        } 
    } 
}

next, nextfile

nextnextfileは多くのひとにとって馴染みがないでしょうが、その機能は非常に単純です。
nextは「今の入力を捨て次の入力の処理に移る」を、nextfileは「今の入力ファイルを捨て次の入力ファイルの処理に移る」を意味しています。

exit

処理を終了します。

BEGIN {
    i = 1;
    if (i == 1) {
        exit 0;
    }
}

実際に試す

BEGINパターンの中にFizzBuzzを書いてみましょう。

解答
    ```awk
    BEGIN {
        for (i = 0; i < 100; i++) {
            if (i % 15 == 0) {
                print "FizzBuzz";
            } else if (i % 3 == 0) {
                print "Fizz";
            } else if (i % 5 == 0) {
                print "Buzz";
            }
        }
    }
    ```

配列

柔軟なプログラムを作成するのに、配列は必要不可欠な要素だといえるでしょう。
AWKの配列は配列ではなく連想配列となっています。

整数型のindexはソートされますが、非整数型のindexはソートされないことに注意が必要です。

BEGIN {
    a[0] = 1;
    a[2] = 2;
    a[1] = 3;
    a[1.4] = "float";
    a["string"] = 0;
    
    for (indx in a) {
        print index;
    }
}

また、indexが配列に存在するかどうかはindx in arrayで確かめることができます。逆に、(array[indx] != "")のようにして確かめることはできません。

関数

関数もあります。
関数の呼び出しは多くのプログラミング言語と同様に、func(arg1, arg2)のような形式を取ります。

関数を自分で定義する

関数の定義にも特に変わった点はありません。例えば、2つの数値を引数にとり、大きいほうの値を返す関数max(x, y)は以下のように定義できます。

function max(x, y) {
    if (x > y) {
        return x;
    } else {
        return y;
    }
}

# 動作確認
BEGIN {
    print max(1.2, 1);
    print max(1.2 2.4);
}

組み込み関数

AWK(とくにgawk)には多くの組み込み関数があります。
ここにすべてを書くのは非常に面倒なので、重要と思えるものの名前だけをここに書いておきます。手抜きでごめんなさい。
詳しくはこのページをみたり、ググったりしてください。

AWKを「頑張って」使う

gawkには多くの機能があるので、テキスト処理以外の様々な用途に使うことができます。
そのときによく話題に上がるのがgawkの並行プロセスの仕組みを利用したネットワークプログラミングです。
ここでは、並行プロセスとネットワークプログラミングについて主に説明します。

並行プロセスを利用したGUIアプリケーション

gawkでは|& というパイプラインと、getlineというコマンドを用いて並行プロセスを起動、標準入出力を介して通信することができます。
その仕組みを利用して、簡単なカウンターアプリケーションを作ってみました。

GUIを表示するために、GTK-serverというコマンドラインツールを使います。
GTK-serverは、GTKのバインディングが存在しない言語でも簡単にGTKを利用できるように作成されたツールです。導入については割愛します。
https://www.gtk-server.org/

gtk-server.cfgというファイルに使用したいGTKの関数やらなんやらを書き、

LIB_NAME = libgtk-x11-2.0.so
FUNCTION_NAME = gtk_init, NONE, NONE, 2, NULL, NULL
FUNCTION_NAME = gtk_window_new, delete-event, WIDGET, 1, LONG
FUNCTION_NAME = gtk_window_set_title, NONE, NONE, 2, WIDGET, STRING
FUNCTION_NAME = gtk_table_new, NONE, WIDGET, 3, LONG, LONG, LONG
FUNCTION_NAME = gtk_container_add, NONE, NONE, 2, WIDGET, WIDGET
FUNCTION_NAME = gtk_label_new, NONE, WIDGET, 1, STRING
FUNCTION_NAME = gtk_label_set_text, NONE, VOID, 2, WIDGET, STRING
FUNCTION_NAME = gtk_table_attach_defaults, NONE, NONE, 6, WIDGET, WIDGET, LONG, LONG, LONG, LONG
FUNCTION_NAME = gtk_button_new_with_label, clicked, WIDGET, 1, STRING
FUNCTION_NAME = gtk_widget_show, NONE, NONE, 1, WIDGET
FUNCTION_NAME = gtk_main_iteration, NONE, WIDGET, 0

|&を多用してGTK-serverと通信すると……

#!/bin/gawk -f

function send_gtk(body) {
    print body |& GTK;
    GTK |& getline result;
    return result;
}

BEGIN{

    GTK = "gtk-server -stdin";

    send_gtk("gtk_init NULL NULL");

    WINDOW = send_gtk("gtk_window_new 0");
    send_gtk(sprintf("gtk_window_set_title %s カウンター", WINDOW));

    TABLE = send_gtk("gtk_table_new 3 3 1");
    send_gtk(sprintf("gtk_container_add %s %s", WINDOW, TABLE));

    LABEL = send_gtk("gtk_label_new 0");
    send_gtk(sprintf("gtk_table_attach_defaults %s %s 1 2 0 1", TABLE, LABEL));

    BUTTON_COUNT = send_gtk("gtk_button_new_with_label カウント");
    BUTTON_RESET = send_gtk("gtk_button_new_with_label リセット");
    send_gtk(sprintf("gtk_table_attach_defaults %s %s 0 1 2 3", TABLE, BUTTON_COUNT));
    send_gtk(sprintf("gtk_table_attach_defaults %s %s 2 3 2 3", TABLE, BUTTON_RESET));

    send_gtk(sprintf("gtk_widget_show %s", LABEL));
    send_gtk(sprintf("gtk_widget_show %s", BUTTON_COUNT));
    send_gtk(sprintf("gtk_widget_show %s", BUTTON_RESET));
    send_gtk(sprintf("gtk_widget_show %s", TABLE));
    send_gtk(sprintf("gtk_widget_show %s", WINDOW));

    EVENT = 0;
    COUNT = 0;

    do {
        send_gtk("gtk_main_iteration");

        EVENT = send_gtk("gtk_server_callback 0");
        if (EVENT == BUTTON_COUNT) {
            COUNT++;
        } else if (EVENT == BUTTON_RESET) {
            COUNT = 0;
        }
        send_gtk(sprintf("gtk_label_set_text %s %s", LABEL, COUNT));
    } while (EVENT != WINDOW);

    close(GTK)
    fflush("")
}

できました。特にgawkを使う利点はなかったように思います。

ネットワークプログラミング

次はネットワークプログラミングです。
gawkでは、なんだかすごい力により、/inet/tcp/0/trap.jp/80のような文字列に対して|&を用いることでネットワークにアクセスしたりすることができます。

/inet/tcp/0/trap.jp/80
は、右から順に「IPv4/IPv6の指定」、「プロトコル」、「ローカルのポート番号」、「通信先のホスト名」、「通信先のポート」となっています。

例えば私のブログblog.fourmisushi.soyを取得するときは

#!/bin/awk -f

BEGIN {
  RS = "rn";
  socket = "/inet/tcp/0/blog.fourmisushi.soy/80";
  print "GET / HTTP/1.0\r\n\r\n" |& socket;
  while ((socket |& getline) > 0) {
    print $0 RT;
  }
  close(socket);
}

とします。
実行結果は以下のようになります。

HTTP/1.1 301 Moved Permanently
Date: Tue, 05 Nov 2019 16:41:34 GMT
Content-Type: text/plain
Content-Length: 31
Connection: close
Location: https:///
x-now-trace: hnd1
server: now
x-now-id: hnd1:d7pb4-1572972094280-843eff80f245
strict-transport-security: max-age=63072000
cache-control: s-maxage=0

Redirecting to https:/// (301)

httpsにリダイレクトされていますが、しっかりとアクセスできていることがわかると思います。

もちろんサーバーも書くことが可能です。
実際にとても簡易なhttpサーバーを書いてみました。
このサーバーのコードは以下のようになっています。

#! /bin/gawk -f

BEGIN {
    RS = ORS = "\r\n";
    HttpService = "/inet/tcp/8080/0/0";

    while (1) {
        time = strftime();
        Len = length(time) + length(ORS);
        print "HTTP/1.0 200 OK" |& HttpService;
        print "Content-Length: " Len |& HttpService;
        print "Content-Type: text/html; charset=UTF-8" ORS |& HttpService;
        print time |& HttpService;
        while ((HttpService |& getline) > 0) {
            continue;
        }
        close(HttpService);
    }
}

実際に通信できることがわかったでしょうか。

あとがき

いかがでしたか?
AWKはただのテキスト処理ツールじゃなくていろんなものがあるプログラミング言語なんだぞ〜って書こうとしたらつまらない文章になってしまったかもしれません。
私自身AWKを知ったのはつい最近なのですが、思っていたよりも機能が多くて驚きました。
AWKを書くときに、そういえばこんな機能があったなーと思い出していただければ幸いです。

明日はK_10p16さんと60さんの記事です。たのしみ〜

この記事を書いた人
Fourmsushi

そこの君もtypoしてみないかい…?

この記事をシェア

このエントリーをはてなブックマークに追加

関連する記事

2019年11月21日
DEATH STRANDING【AdC2019 22日目】
Amanogawa
2019年11月20日
AAっぽい動画を作る【Advent Calendar 2019 21日目】
hosshii
2019年11月20日
キラッとプリ☆チャンつくってみた!
d_etteiu8383
2019年11月19日
Advent Calendar 2019 20日目
Adwaver_4157
2019年11月18日
C++スタイルのキャスト演算子を使おう!【アドベントカレンダー2019 19日目】
kegra
2019年11月17日
据え置きの音をお外に持ち出そう
liquid1224

活動の紹介

カテゴリ

タグ