この記事は 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に近いのはKernighanによる
nawk
です。 mawk
はMike Brennanによる高速なAWKです。gawk
、つまりGNU AwkはオリジナルのAWKよりも多くの機能を持ち、TCP/IPの通信などを行うことができます。
主にAWKはテキスト処理に使われる言語で、他の多くのプログラミング言語とは異なり「データ指向」と呼ばれることがあります。
現代でも、コマンドラインでsed
やcut
などの補助としてしばしば用いられています。
この記事では、そんな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
という入力の場合$0
は1 a
、$1
は1
、$2
はa
です。
以下のコマンドで確かめてみましょう。
echo "1 a" | awk '{ print $0; print $1; print $2;}'
パターンの種類
パターンとして使えるものは以下のとおりです。
(例で使われている、引数のないprint
はprint $0
と同じ出力をします。)
- 表現
- その値が0でないまたはnullでない値のとき
- 例:
$1 { print }
- 入力の1フィールド目が0、nullでないとき、その行を出力します。
- 例:
$2 > 12 { print }
- 入力の2フィールド目の値が12より大きいとき、その行を出力します。
- 正規表現
- 入力の一部が正規表現にマッチしたとき
- 例:
/[0-9]/ { print }
- 入力が数字を含むとき、その行を出力します。
- 例:
$1 ~ /[A-Za-z0-9]+/ { print }
$n ~ /regexp/
と書くことで、正規表現でチェックするフィールドを指定することができます。- 入力の1フィールド目がアルファベットまたは数字を含むとき、その行を出力します。
- 組み込みパターン
BEGIN
/END
:処理の最初と最後BEGINFILE
/ENDFILE
:1つ以上のファイルから入力したとき、ファイルの最初と最後
- 範囲
- ひとつ目のパターンにマッチしてから、ふたつ目のパターンにマッチするまでの間
- 例:
$1 == "2019-10-01", $1 == "2019-10-31" { print }
- 入力の1フィールド目が2019-10-01の行から2019-10-31の行までを出力します。
- なし
- すべての入力にマッチします。
実際に試す
習うより慣れろ。
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には多くの組み込み変数があります。
ここではその全てを説明することはしませんが、代表的・重要なものを以下に示します。
- FS
- フィールドの区切り文字で、デフォルトは半角スペースです。
- OFS
- 出力の区切り文字で、デフォルトは半角スペースです。
- RS
- 入力行の区切り文字で、デフォルトは改行(\n)です。
- ORS
- 出力行の区切り文字で、デフォルトは\nです。
- NF
- 処理している行のフィールドの数
- NR
- いままでに処理した行の数
- FPAT
- フィールドの区切り文字を表す正規表現で、FSよりも多くの場合に対応できます。
実際に試す
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
next
とnextfile
は多くのひとにとって馴染みがないでしょうが、その機能は非常に単純です。
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)には多くの組み込み関数があります。
ここにすべてを書くのは非常に面倒なので、重要と思えるものの名前だけをここに書いておきます。手抜きでごめんなさい。
詳しくはこのページをみたり、ググったりしてください。
- int
- rand
- asort, asorti
- gensub, gsub
- index
- length
- match
- split
- sprintf
- sub, substr
- system
- and, or, xor
- typeof
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さんの記事です。たのしみ〜