シェル・ワンライナー 160 本ノックを完走した

この記事は、 NTT Communications Advent Calendar 2022 4 日目の記事です。

こんにちは。 SDPF クラウド・仮想サーバーチームの杉浦です。 普段は OpenStack の開発・運用をしています。

みなさんはシェル芸と聞いてどのようなコマンドを想像しますか?

私は以下のような怖いコマンド 1 を想像していました

# 無限に process を fork するコマンドです
# 実行するときは自己責任でお願いします

:(){ :|:& };:

ですがシェル芸はもっと親しみやすくて 2 実用的なものです。 私はシェル芸のシェの字もできないくらいシェル芸初心者だったのですが、 1日1問、半年以内に習得 シェル・ワンライナー160本ノック という本を完走してシェル芸チョットワカルようになったので、本の宣伝をしつつ完走した感想を紹介しようと思います。

1日1問、半年以内に習得 シェル・ワンライナー160本ノック https://gihyo.jp/book/2021/978-4-297-12267-6

本を読む前は

本を読む前はシェル芸こそできなかったものの、 CLI 環境で十分生活できるくらいには bash とお友達でした。 catlessgrep などの基本的なコマンドも使えましたし、 vim も難なく使いこなせていました。

ただ、例えば awk はほとんど使ったことがありませんでした。 xargs に関しては Stack Overflow ではたまにみるけどどういう動作をしているのかきちんと理解していませんでした。 テキストファイルの中身を解析するときは VSCode やスプレッドシートに貼り付けてなんとかしていました。

シェル芸をやってみようと思ったきっかけ

ログファイルの解析やサーバーの管理の際にちょっとしたテキスト操作ができなくてもどかしい思いをしたからです。

仮想サーバーチームでは数千台規模の仮想サーバーを運用しています。 多数のサーバーを効率的に運用するために、開発用端末から SSH 経由でコマンドを叩いてサーバーの管理をするわけです。

あるとき、サーバーに搭載されているメモリ量を調べたいことがありました。 Linux では free コマンドを使うとメモリの使用状況を調べることができます。

$ free
              total        used        free      shared  buff/cache   available
Mem:       12235124      290432     4185192        5088     7759500    11720024
Swap:             0           0           0

調査対象が 1 台であれば Mem 行の total 列の数字をターミナルからコピーすればよいですが、複数のサーバーを横断的に調べたいときは Mem 行の total 列の数字を取り出す操作を自動化したくなります。

シェル芸がチョットワカルようになった今だったらいくつもの方法が考えられますが、当時は awk コマンドに馴染みがなかったのでコマンドがすっと出てこずに苦労しました。 どうやって解いたかはあまり覚えていませんが、 grep を使ったりスプレッドシートを使ったりしてなんとかしのいだ覚えがあります。

シェル芸本の読み方

シェル芸本では、シェル芸とは「Unix 系 OS のシェル上でワンライナーのコマンドを駆使すること」と定義されています。 冒頭で示したようなワンライナーだけではなく、日常の業務をスッと終わらせるために使うコマンドもシェル芸とみなせます。

この本では実践形式で手を動かしながらシェル芸を学びます。 コマンドの使い方を説明したあとに問題が出され、解説があります。 最初は問題があまり解けずつらい気持ちになるかもしれませんが、シェル芸の型が身につくとみるみる解けるように解けるようになって楽しいですよ。

最初は echols コマンド、 Control + C の使い方から始まり、シグナルやファイルシステム、システムコールといった発展的なトピックも出てきます。 また、問題は初級・中級・上級に分かれていて自分のレベルに合わせて取り組むことができます。 Unix 初心者の方から上級者の方まで楽しむことができますね。

シェル芸本は 3 部構成になっています。

  • 第 1 部: シェルとコマンドに親しむ
  • 第 2 部: 発想力を鍛える
  • 第 3 部: 応用する

シェル芸初心者の方は最初から解いていくのがよいでしょう。 ただし、すべて解こうとすると時間がかかるので、必要なところをかいつまんで読んでもいいかもしれません。 例えば 文字コードとバイナリ で学ぶ内容は他の章ではあまり出てこないので飛ばしてしまってもいいでしょう。 第 3 部は第 1 部、第 2 部で身につけた知識を実際のサーバーの解析で応用できるかどうかを問う問題が出ます。 シェル芸に自身がある方は第 3 部から挑戦して、知識に不足があれば必要に応じで第 1 部、 2 部を参照する、といったやり方でもよさそうです。

シェル芸の構成要素

シェル芸は以下の要素が絡み合って構成されていると感じました。

  • 基本コマンドの使い方
  • Bash の便利機能
  • オプション
  • 便利なコマンド
  • 正規表現の使い方
  • コマンドの組み合わせ方のパターン
  • アート要素

基本コマンドの使い方

以下のコマンドはシェル芸でよく使います。 マニュアルやチートシートを見なくても使えるようにしておきましょう。

  • awk
  • sed
  • grep
  • xargs
  • wc
  • sort
  • uniq
  • find
  • cat
  • date

基本的なコマンドの使い方は 第 1 章で解説があります。

Bash の便利機能

man bash すると bash の機能を確認できます。 例えば、以下の機能は知っておくと便利です。

  • Process Substitution
    • <(command)
    • >(command)
  • Brace Expansion
    • {n..m}
    • {a,b,c}
  • History Expansion
    • fc
    • !n
    • ^CommandA^CommandB^
  • Parameter Expansion
    • ${parameter:-word}
    • ${parameter:offset:length}
    • ${parameeter#word}
    • ${parameter/pattern/string}

bash の機能は第 2 章で詳しく学びます。

便利なコマンド

awk を使えば大抵のことはできますが、便利なコマンドを知っているとワンライナーをシンプルにできます。 複雑な処理をしたいときは perlpython を使ったほうがいいかもしれません。 シェル芸の本を読んで一度は使ってみるとよいでしょう。

  • paste
  • join
  • dateutils
  • zgrep
  • xzgrep
  • jq
  • gron
  • bc
  • printf
  • perl
  • ruby
  • teip3

オプション

オプションを使うとコマンドの動作や出力を制御できます。適切なオプションを知っているとコマンドの出力を加工する手間を省くことができます。 例えばシェル芸本では grep のオプションとして以下のものを使います。知らないオプションはありませんか?

  • A
  • a
  • B
  • C
  • E
  • f
  • H
  • m
  • n
  • o
  • q
  • v
  • x
  • z

正規表現

正規表現にはそれだけで本が書けてしまうくらい4機能が豊富にあります。 シェル芸本でも正規表現を多用します。

grep では -P オプション を使うと Perl の強力な正規表現 Perl-compatible regular expressions (PCREs) を使えます。また ruby にも強力な正規表現エンジン oniguruma5 が内蔵されています。

grep -Pruby では以下のような強力な機能が使えます。

  • メタ文字
    • \d: 数字にマッチする
    • \p{Han}: 漢字にマッチする
  • 後方参照
    • \1\2 ....
  • 先読み・後読み・否定先読み・否定後読み
    • (?=pattern)
    • (?<=pattern)
    • (?!pattern)
    • (?<!pattern)
  • 部分式呼び出し
    • \g<...>

正規表現は第 3 章で詳しく取り扱います。

コマンドの組み合わせ方のパターン

Unix では単純なコマンドを組み合わせることで複雑な仕事を片付けることができます。 シェル芸では個々のコマンドやオプションの動作を覚えるのも大事ですが、コマンドの組み合わせ方も学ぶ必要があります。

例えば sort してから uniq するのは頻出パターンです。 wordlist.txt からユニークな単語を調べたいときは

$ cat wordlist.txt | sort | uniq

となりますね。

grep -o してから uniq するのもよく使います。 例えば story.txt からある単語の出現回数を知りたいときは

$ cat wordlist.txt | grep -o the | uniq -c

となります。

プロセス置換も使えると便利です。たとえば headtail の出力を組み合わせたいときは以下のようになります。

$ cat <(head story.txt) <(tail story.txt)

コマンドの組み合わせ方はシェル芸本全体を通して学びます。

アート要素

シェル芸本の中にはシェル芸っぽい解答もあります。 問題 29 には sort | uniq の代替として

awk '!a[$1]'

が紹介されています。 a[$1]$1 の出現回数を数えるのですが初回は a[$1] が 0 なので、 ! を使うと初回のみ条件が成立して print されるというわけです。賢いですね。

他にも vim をコマンドとして使う 解答もあったりします。例えば問題 31 を参照してください。

取り組んでみてどうなったか

bash での生活がかなり豊かになりました。

具体的には、ワンライナーがすぐに作れるようになりました。 今まではシェルで込み入った処理をする場合、インターネットでよさそうなワンライナーを探したり、コマンドやオプションの意味を調べたりする必要があって、本質的でないことに時間がかかっていたように思います。 今では必要があれば man を参照するくらいで、かなりストレスフリーにワンライナーを書くことができます。 複数の解法を思いつくこともしばしばあります。

例えば、 free コマンドの出力のうち Mem 行の total 列の数字を取り出すワンライナーは次が考えられます。

$ free | grep Mem: | awk '{print $2}'
$ free | xargs | awk '$0=$8'
$ a=($(free | sed -n 2p)); echo ${a[1]}
$ cat /proc/meminfo  | grep -oP 'MemTotal:\s+\K\d+'

また、複雑な操作をワンライナーとして表現できるようになったのも嬉しいポイントです。 ssh コマンドでは ssh hostname cat /etc/passwd とすることで SSH 越しにコマンドを叩くことができます。 ワンライナーを書くことができれば多数のホストに対してコマンドを実行できるようになるので嬉しいわけです。

シェル芸が役立った実例

お仕事でシェル芸が役立った実例を紹介します。

仮想サーバーチームの業務として、仮想化ソフトウェア qemu/KVM の管理業務があります。 以前 VM の動作が不調だという問い合わせを受けたときに VM の動作をホスト側から解析したことがありました。 qemu には trace-events 6 という VM がホスト側に発行するイベントをトレースする仕組みがあります。

trace-events を有効にすると、次のようなログを得られます。

20480@1663636838.696500:virtio_blk_handle_write vdev 0x55dc8a720ff0 req 0x55dc8a39c820 sector 12437488 nsectors 16
20480@1663636838.696531:blk_co_pwritev blk 0x55dc897db7f0 bs 0x55dc897dba50 offset 6367993856 bytes 8192 flags 0x0
20480@1663636838.699222:virtio_blk_rw_complete vdev 0x55dc8a720ff0 req 0x55dc8a39c820 ret 0
20480@1663636838.699231:virtio_blk_req_complete vdev

この中から特定の時間帯のログを取り出すにはどのようにすればよいでしょうか?

時刻の情報を取り出す

まずは各行から時刻の情報を取り出したいです。 ログをよく見ると 20480@1663636838.696500 という出力が見つかります。

問題 68 で学ぶように、 @数値 という形式は Unix 時刻を表すときに使います。 次のようにすると Unix 時刻を読み込んで所定のフォーマットで出力できます。

$ date -d '@2147483647'
Tue Jan 19 03:14:07 UTC 2038

よって 20480@1663636838.696500@ より後ろの部分は時刻を表していると推測できます。 @ より前はおそらく process id です。

awk でログをフィルタリングする

まずは awk で処理しやすいように @: をスペースに変換します。

$ cat log.txt  | tr '@:' '  '
20480 1663636838.696500 virtio_blk_handle_write vdev 0x55dc8a720ff0 req 0x55dc8a39c820 sector 12437488 nsectors 16
20480 1663636838.696531 blk_co_pwritev blk 0x55dc897db7f0 bs 0x55dc897dba50 offset 6367993856 bytes 8192 flags 0x0
20480 1663636838.699222 virtio_blk_rw_complete vdev 0x55dc8a720ff0 req 0x55dc8a39c820 ret 0

次に 2 列目をよく見て特定の時間内のログを取り出せばよいでしょう。

date コマンドは日時の出力フォーマットを変更できます。 Unix 時刻の形式で出力したいときは +%s を指定します。

これを使って、開始時刻を

$ date -d '2022/09/20 01:20:40' +%s
1663636840

終了時刻を

$ date -d '2022/09/20 01:21:00' +%s
1663636860

としてみましょう。

awk コマンドは -v オプションを使うと awk プログラムの中で使える変数を定義できます。 時刻は $2 で参照できるので、次のようにすれば start から end までのログを取り出せますね。

$ cat log.txt  | tr '@:' '  ' | awk -v start=$(date -d '2022/09/20 01:20:40' +%s) -v end=$(date -d '2022/09/20 01:21:00' +%s) 'start < $2 && $2 < end'
20480 1663636852.706415 virtio_blk_handle_write vdev 0x55dc8a720ff0 req 0x55dc8b19b6f0 sector 8951968 nsectors 16
20480 1663636852.706452 blk_co_pwritev blk 0x55dc897db7f0 bs 0x55dc897dba50 offset 4583407616 bytes 8192 flags 0x0
20480 1663636852.707988 virtio_blk_rw_complete vdev 0x55dc8a720ff0 req 0x55dc8b19b6f0 ret 0
20480 1663636852.708002 virtio_blk_req_complete vdev 0x55dc8a720ff0 req 0x55dc8b19b6f0 status 0

後で Unix 時刻の前に @ がついていると都合がいいのでつけておきましょう。

$ cat log.txt  | tr '@:' '  ' | awk -v start=$(date -d '2022/09/20 01:20:40' +%s) -v end=$(date -d '2022/09/20 01:21:00' +%s) 'start < $2 && $2 < end{$2="@"$2; print}'
20480 @1663636852.706415 virtio_blk_handle_write vdev 0x55dc8a720ff0 req 0x55dc8b19b6f0 sector 8951968 nsectors 16
20480 @1663636852.706452 blk_co_pwritev blk 0x55dc897db7f0 bs 0x55dc897dba50 offset 4583407616 bytes 8192 flags 0x0
20480 @1663636852.707988 virtio_blk_rw_complete vdev 0x55dc8a720ff0 req 0x55dc8b19b6f0 ret 0
20480 @1663636852.708002 virtio_blk_req_complete vdev 0x55dc8a720ff0 req 0x55dc8b19b6f0 status 0

時刻のフォーマットを変更する

今までのワンライナーでログのフィルタリング自体はできましたが、時刻の表記が見づらいです。 $2 に対して date を適用して見やすくしたいですよね。 このようなときは teip コマンドが便利です。

$ teip -f 2 -- command

とすると $2 に対してコマンド command が適用されます。

date-f オプションを使うとファイルから時刻表現を読み取って解釈してくれます。標準入力から読み込むには - というファイル名を指定します。

ここでは $2 に対して date -f- '+%F %T.%N' を適用してみましょう。

$ cat log.txt  | tr '@:' '  ' | awk -v start=$(date -d '2022/09/20 01:20:40' +%s) -v end=$(date -d '2022/09/20 01:21:00' +%s) 'start < $2 && $2 < end{$2="@"$2; print}' | teip -f 2  -- date -f- '+%F %T.%N'
20480 2022-09-20 01:20:52.706415000 virtio_blk_handle_write vdev 0x55dc8a720ff0 req 0x55dc8b19b6f0 sector 8951968 nsectors 16
20480 2022-09-20 01:20:52.706452000 blk_co_pwritev blk 0x55dc897db7f0 bs 0x55dc897dba50 offset 4583407616 bytes 8192 flags 0x0
20480 2022-09-20 01:20:52.707988000 virtio_blk_rw_complete vdev 0x55dc8a720ff0 req 0x55dc8b19b6f0 ret 0
20480 2022-09-20 01:20:52.708002000 virtio_blk_req_complete vdev 0x55dc8a720ff0 req 0x55dc8b19b6f0 status 0

先頭に process id がついているのが気になりますが、これで OK とします。

最後に

達人プログラマー 7 の第 3 章では道具に習熟する大切さを説いています。

道具はあなたの能力を増幅します。道具のできが優れており、簡単に使いこなせるようになっていれば、より生産的になれるのです

シェル芸本はかなりボリュームがあってそれなりに時間がかかりますが、 bash で生活している人は読んでおいて損はないと思います。みなさんもぜひシェル芸を身に着けて bash ともっと仲良くなりましょう!

仮想サーバーチームでは冬期インターンシップのポストを募集しています。 大規模なクラウドの裏側を知りたい、業務でシェル芸を使ってみたいと思ったらぜひご検討ください。 たくさんのご応募お待ちしています!

engineers.ntt.com

information.nttdocomo-fresh.jp

それでは、明日の投稿もお楽しみに。

© NTT Communications Corporation All Rights Reserved.