awkでできる、一行野郎

タグ: awk   gawk  

この記事は、Awkで何ができるかをざっと説明するものです。Linuxの世界で言うところの「一行野郎(ワンライナー)」で紹介します。一行野郎とは、スクリプトプログラムをテキストファイルとして組むのではなく、コマンドラインで指定する方法の通称です。

データファイル

Awkが得意なのは、テキストファイルの加工です。今回はls -laの出力をサンプルにします。出力内容をdata.txtにリダイレクトし、テキストファイルにしました。(注意:出力フォーマットや内容は設定により異なります。今回の例は日本語設定のopenSUSEでの出力を例にしています。)

data.txt

drwxrwsr-x  8 hiro www-data  4096  5月  5  2017 .
drwxr-xr-x 12 hiro hiro      4096  4月 25 06:53 ..
-rw-rw-r--  1 hiro www-data    38  8月  6  2016 .bowerrc
-rw-rw-r--  1 hiro www-data   147  8月  6  2016 .directory
-rw-rw-r--  1 hiro www-data    35  8月  6  2016 .htaccess
drwxrwsr-x 13 hiro www-data  4096  8月  6  2016 .sass-cache
drwxrwsr-x  8 hiro www-data  4096  8月  6  2016 bower_components
-rw-rw-r--  1 hiro www-data  1769  9月  6  2016 generate-sitemap.sh
-rw-rw-r--  1 hiro www-data    60  8月  6  2016 gzip-one-file.sh
-rw-r--r--  1 hiro www-data   274 10月 27  2016 index.md
drwxrwsr-x  5 hiro www-data  4096  2月  6  2017 markdown
drwxr-xr-x 12 hiro www-data  4096  2月 21 17:27 public
drwxrwsr-x  2 hiro www-data  4096  5月 19  2017 scss

実際の出力は1行目に「合計 104」のような、表示ファイルの使用ブロック数の合計がでてきますが、それを削除しています。sed '1d'で削除できますので、自分のファイルシステムで試す場合は、ls -la | sed '1d'で試してください。

一行野郎の代表例

Awkはテキストの各行をフィールド(項目)として扱うのが得意です。

デフォルトではフィールドは、空白で区切られた項目のことです。空白は一つでも、複数でも構いません。

  • ファイル名(9つ目のフィールド)のみ表示する
$ awk '{print $9}' data.txt
.
..
.bowerrc
.directory
.htaccess
.sass-cache
bower_components
generate-sitemap.sh
gzip-one-file.sh
index.md
markdown
public
scss

Unix文化で生まれたAwkは、もちろんテキストのフィルタリングを行いやすくするため、パイプで繋いでテキスト処理が行えます。

$ cat data.txt | awk '{print $9}' | sort -r
scss
public
markdown
index.md
gzip-one-file.sh
generate-sitemap.sh
bower_components
.sass-cache
.htaccess
.directory
.bowerrc
..
.

catコマンドでファイルの内容をawkに渡し、awkの出力をsortコマンドへ送っています。-rオプションで逆順に表示しています。

  • タイムスタンプを月日で表示する
$ awk '{print $6, $7}' data.txt
5月 5
4月 25
8月 6
8月 6
8月 6
8月 6
8月 6
9月 6
8月 6
10月 27
2月 6
2月 21
5月 19

print文のフィールドをカンマで区切ると、スペースを開けて表示されます。スペースを開けずに詰めて表示する場合は、カンマを挟まずに列挙します。

$ awk '{print $6 $7 "日"}' data.txt
5月5日
4月25日
8月6日
8月6日
8月6日
8月6日
8月6日
9月6日
8月6日
10月27日
2月6日
2月21日
5月19日

上記のとおりに、文字列は""で囲みます。今度は月/日形式で表示してみましょう。

$ awk '{print gensub("月", "/", "1", $6) $7}' data.txt
5/5
4/25
8/6
8/6
8/6
8/6
8/6
9/6
8/6
10/27
2/6
2/21
5/19

文字列置換関数の一つであるgensubを利用して、「月」を/に置換しました。gensubの第1引数は正規表現も使用できます。同じ内容を正規表現を使用すると、以下のようになります。

$ awk '{print gensub(/月/, "/", "1", $6) $7}' data.txt

正規表現はスラッシュで囲みます。他の言語では"/.../"の形式で文字列として定義することが多いですが、"..."で囲んではいけません。はまりがちなポイントです。

正規表現を/.../で囲まずに、文字列として"..."だけで囲んでも動作します。動的に変数に正規表現を代入してから、マッチングに利用することも可能です。

今までは、全行を対象に処理を行いました。では、次は処理・出力の対象行を限定しましょう。

  • 所属グループが"hiro"のファイル名表示
awk '$4 == "hiro" { print $9 }' data.txt
..

基本的にawkは、'パターン { 処理内容 }'形式のスクリプトを必要なだけ記述できます。パターンとは処理の対象を絞り込む条件です。処理の対象を指定しない場合は、今までの実行例のように全行が処理対象です。

今回は処理の対象行を$4 == "hiro"という、シンプルな条件比較で指定しました。

  • 所属グループが"hiro"以外のファイル名表示
$ awk '$4 != "hiro" { print $9 }' data.txt
.
.bowerrc
.directory
.htaccess
.sass-cache
bower_components
generate-sitemap.sh
gzip-one-file.sh
index.md
markdown
public
scss

一致を検査する==の代わりに、不一致を検査する!=を使いました。

  • 行のどこかに8が含まれている行を表示
$ awk '/8/ { print $0 }' data.txt
drwxrwsr-x  8 hiro www-data  4096  5月  5  2017 .
-rw-rw-r--  1 hiro www-data    38  8月  6  2016 .bowerrc
-rw-rw-r--  1 hiro www-data   147  8月  6  2016 .directory
-rw-rw-r--  1 hiro www-data    35  8月  6  2016 .htaccess
drwxrwsr-x 13 hiro www-data  4096  8月  6  2016 .sass-cache
drwxrwsr-x  8 hiro www-data  4096  8月  6  2016 bower_components
-rw-rw-r--  1 hiro www-data    60  8月  6  2016 gzip-one-file.sh

gensub関数の第1引数で正規表現の説明をしましたが、パターンの/8/は正規表現です。正規表現に一致するかの正式な記述は、$0 ~/8/と書きますが、個別のフィールドの代わりに一行丸ごとを示す$0に含まれる絞り込みの場合は、省略可能です。

  • ファイル名がピリオドで始まる行を表示
$ awk '$9 ~/^\./' data.txt
drwxrwsr-x  8 hiro www-data  4096  5月  5  2017 .
drwxr-xr-x 12 hiro hiro      4096  4月 25 06:53 ..
-rw-rw-r--  1 hiro www-data    38  8月  6  2016 .bowerrc
-rw-rw-r--  1 hiro www-data   147  8月  6  2016 .directory
-rw-rw-r--  1 hiro www-data    35  8月  6  2016 .htaccess
drwxrwsr-x 13 hiro www-data  4096  8月  6  2016 .sass-cache

「ファイル名がピリオドで始まる」は正規表現では、/^\./です。^が行頭、\.がピリオドを表しています。ピリオドは正規表現の中では、「一文字」を表す特別な記号ですので、それを文字としてのピリオドを表すためにバックスラッシュを先頭に付け、エスケープしています。基本的な正規表現は他の言語と大して違いがありません。

もう一つのポイントは、{ print $0 }を記述していないことです。パターンのみを記述し、{ 処理内容 }を省略すると、行の内容を表示します。

  • 名前の先頭がピリオドで始まるディレクトリを表示
$ awk '$1 ~/^d/ && $9 ~/^\./' data.txt
drwxrwsr-x  8 hiro www-data  4096  5月  5  2017 .
drwxr-xr-x 12 hiro hiro      4096  4月 25 06:53 ..
drwxrwsr-x 13 hiro www-data  4096  8月  6  2016 .sass-cache

ファイルのパーミッションを表す第1フィールドの先頭が、ディレクトリの場合はdです。それを直前のサンプルの正規表現と、AND条件を表す&&で接続しました。

  • タイムスタンプが2月の行を表示
$ awk '$6 == "2月"' data.txt
drwxrwsr-x  5 hiro www-data  4096  2月  6  2017 markdown
drwxr-xr-x 12 hiro www-data  4096  2月 21 17:27 public
  • タイムスタンプが2月の行の件数を表示
$ awk '$6 == "2月" { count++ } END { print count }' data.txt
2

Awkで変数は宣言しません。countは変数で、カウンターとして使用しています。最初に参照されたときは未定義ですが、++で1を足し込む時点で、算術計算時の未定義は0扱いになり、結果0+11になります。

もう一つの注目ポイントはENDというパターンです。この大文字のENDは、行の処理が全部終わったことを表すパターンです。簡単に言えば、最後に一回実行される処理を記述したい場合に利用します。

タイムスタンプが「2月」の行をカウントし、最後に結果を表示しています。

  • タイムスタンプが2月のファイル名をyyyy/mm/dd形式のタイムスタンプ付きで表示する
$ awk '$8 ~/:/ { $8 = "2018" } $6 == "2月" { gsub("月", ""); print $8 "/" $6 "/" $7, $9 }' data.txt
2017/2/6 markdown
2018/2/21 public

今年(記述現在で2018年)のタイムスタンプの年では、時刻が表示されます。タイムスタンプが「2月」の処理中でif文を使用し記述することもできますが、ワンライナーでは読み書きしづらいため、パターン指定で頑張っています。

タイムスタンプの年が時刻であるかを:が含まれているかで判定し、該当する行の8つ目のフィールドを"2018"に置き換えています。$数字はフィールドを表していますので、代入することで該当フィールドの内容を置き換えられます。

gsub("月", "")は文字列置換の関数です。一致する文字をすべて置き換えます。第1引数が文字列か正規表現、第2引数が置き換え後の文字列です。第3引数は対象ですが、省略すると$0がデフォルトで指定されます。この関数は、破壊的です。つまり、対象のフィールドや変数を直接置き換えてしまいます。

パターンと処理内容のアクションが2つだけですが、複雑に見えます。長いワンライナーが苦手な方は、パイプでつなぐ手もあります。同じことをパイプで繋いでみましょう。

$ awk '$8 ~/:/ { $8 = "2018" } { print $0 }' data.txt | awk '$6 == "2月" { gsub(" 月" ""); print $8 "/" $6 "/" $7, $9 }'
2017/2/6 markdown
2018/2/21 public

基本的にスクリプトが長くなる場合は、ファイルで組んだほうが読み書きしやすいです。しかし、一回だけ使用する使い捨てのスクリプトが長くなる場合は、パイプを使う方法もあるのを覚えておくと役立ちます。

さて、残念ながら、月日の表示が厳密にはmm/ddの2桁ずつになっていません。Awkでも、他言語でサポートされているフォーマット付き出力のprintf文が使用できます。

$ awk '$8 ~/:/ { $8 = "2018" } $6 == "2月" { gsub("月", ""); printf "%04d/%02d/%02d %s\n", $8, $6, $7, $9 }' data.txt
2017/02/06 markdown
2018/02/21 public

出力フォーマットの最後の\nを忘れないようにと言うポイントも、多言語と同じです。

ここまで説明してきた知識で、私がAwkを使用するパターンの80%はカバーしています。テキスト出力の加工の目的は、抽出とフォーマット変更ですので、そのために開発されたAwkがいまだ多くのプログラマーに愛されているのが理解していただけたでしょうか。