awkでできる、一行野郎
この記事は、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+1
で1
になります。
もう一つの注目ポイントは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がいまだ多くのプログラマーに愛されているのが理解していただけたでしょうか。