4月入社の新人エンジニアのxztaityozxです。趣味はdotfilesいじりです
皆さんは日々の開発でふと、調査やデータの加工が必要となったことはありませんか?私はたくさんあります。
ではそういった時、どうやって解決していますか?私はいくつかのCLIツールを組み合わせることで解決しています。
例えば、/etc/services
からtcpなサービスの名前を取り出すときは以下のようにします
$ cat /etc/services | grep -P "\d+/tcp" | awk '{print $1}'
こういったテクニックを「ワンライナー」と呼ぶことにし、日常のちょっとした調査やデータ加工をサッと出来るようにコマンドラインと触れ合っていくことを目的とした勉強会「ワンライナー勉強会」を開催しました。
この記事では、「ワンライナー勉強会」の進め方や問題、解答例を紹介します。
進め方
社内勉強会は開催者が自由にその形式を決めて良いとのことなので、プライベートでよく参加しているシェル芸勉強会を参考に、以下のような進め方にしました。
- 参加者に問題を提示する
- 参加者は10分間でその問題にチャレンジする
- このとき使って良いのはコマンドラインのみ
- 解答ができたから、Slackへ投稿する
- このとき私が解説できそうなものは解説します
- 解答例を示す
- 1~4を繰り返す
作問はUbuntu 20.04のbash v5.0.16 で行いました。
問題
今回の勉強会のためにいくつか作問しましたが、この記事ではその中から2問紹介したいと思います。この記事を読んでいるみなさんも是非挑戦してください!
Q1 30までのFizzBuzzを出力してください
FizzBuzzとは、3の倍数のときはFizz
、5の倍数ではBuzz
、15の倍数ではFizzBuzz
と発言するパーティーゲームです。よくプログラミングの入門としても出題されますね。
参加者の解答として投稿されたものをいくつか紹介します。
$ seq 30 | awk '{ if ($1 % 15 == 0) { print "FizzBuzz" } else if ($1 % 3 == 0) { print "Fizz" } else if ($1 % 5 == 0) { print "Buzz" } else { print $1 } }'
スクリプト言語であるawkを使った解答です。FizzBuzzの要件をそのまま落とし込んだ!という感じの解答でとても読みやすいです。
$ python3 -c "for i in range(1,31):print('Fizz'*(i%3==0)+'Buzz'*(i%5==0) or str(i))" $ python -c "[print(('Fizz' if i % 3 == 0 else'') + ('Buzz' if i % 5 == 0 else '') or i) for i in range(1, 31)]"
こちらはPythonを使った解答です。両者とも「文字列を組み立てて出力する」タイプで、1つ目の解答は文字列の繰り返しを上手く使った解答でした。
この他にも、Perlやnodeを使った解答もありました。同じ問題を解くにしても、いろんなアプローチが見られるのがワンライナーの面白いところです。
ちなみに私が用意してきた解答例は
$ seq 30 | awk 'NR%3==0{printf "Fizz"}NR%5==0{printf "Buzz"}{print " "$1}' | awk '{print $1}'
というものです。1回目のawkではデータの加工、2回目のawkではデータの抽出を行っています。
Q2 nums
というファイルには、100,000個の数値が改行区切りで書いてあります。これの総和を求めてください。
10万行の数値データの総和を求める問題です。皆さんはどういう解答をされたのでしょうか。以下に示します。
$ cat nums | awk 'BEGIN{s = 0} {s += $1} END{print s}' $ cat nums | python -c 'import sys; print(sum(map(int, sys.stdin.readlines())))' $ ghc -e 'do f <- fmap (\x -> map read (lines x)) (readFile "nums"); print $ sum f'
この問題もawk,python,haskellなど、たくさんの方法での解答が投稿されました。特に気になったのは以下の解答です。
$ echo $(tr '\n' '+' < ./nums) 0 | bc
tr
で数式を組み立て、bc
コマンドで数式を評価するというものです。少し詳しく解説してみます。かんたんのためにnums
ファイルの中身は以下のとおりとします
$ cat nums 12726 24406 17542 30717 14595 9099 10860 9689 29765 122
これに対して解答にあるtr
コマンドを実行してみます。
$ tr '\n' '+' < ./nums 12726+24406+17542+30717+14595+9099+10860+9689+29765+122+
数式が出来ました。ただし、最後の改行も+
に置換されているので、式が成り立っていません。このままでは bc
に渡したところでエラーが返ってきてしまいます。
$ tr '\n' '+' < ./nums | bc (standard_in) 1: parse error
なので、末尾に0
を付けて式を成立させます。
$ echo $(tr '\n' '+' < ./nums) 0 12726+24406+17542+30717+14595+9099+10860+9689+29765+122+ 0
$()
はコマンド置換と呼ばれるものです。$()
でくくったコマンドの結果を別のコマンドに埋め込むことが出来ます。この場合では、tr
コマンドの結果をecho
に埋め込んでいます。これにより無事式が成立し、bc
で評価することが出来るようになりました。
$ echo $(tr '\n' '+' < ./nums) 0 | bc 159521
とても素敵な解答でした。
ちなみに100,000個の数値データは以下のようなワンライナーで生成しました。この問題に挑戦するときにお使いください。
$ seq 100000 | while read _;do echo $RANDOM; done
まとめ
初めての開催でしたが、たくさんの参加者が来てくれ、積極的に解答してもらえて嬉しかったです。参加者からも「楽しかった」「面白かった」と言ってもらえたので次回開催のモチベーションになりました。問題の難易度や時間配分を調整してまた開催したいです。