話題の記事

シェル芸勉強会の問題を紐解いてみた

2018.04.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

中山(順)です

みなさん、シェル芸ってご存知ですか? ワンライナーで人をあっと驚かせるような出力を行うこと、それがシェル芸。(たぶん)

シェル芸 - アンサイクロペディア

私は何を思ったのか、ふらりとシェル芸勉強会に参加してみました。

jus共催 第35回またまためでたいシェル芸勉強会

そこは私の知らない世界でした。

出題された問題

勉強会は予め主催者側で用意された問題をみんなで問いていくという形で進められました。

まずはどんな問題が出題されたか見てください。

【問題のみ】jus共催 第35回またまためでたいシェル芸勉強会

        |\           /|
        |\\       //|
       :  ,> `´ ̄`´ <  ′
.       V            V
.       i{ ●      ● }i
       八    、_,_,     八    わけがわからないよ 
.       / 个 . _  _ . 个 ',
   _/   il   ,'    '.  li  ',__

そんな気持ちでした。本当に全く手が出せませんでした。 そんなわけで早々に観戦モードだったのですが、twitterを見ていると時間内に回答する猛者たちが存在するわけです。

ハッシュタグ #シェル芸

世界って広いですね!ただただ、リスペクトするしかありません。

とは言っても全く解けないのは悔しいので、1問くらいは理解しておきたいと思い回答例を1つだけ紐解いてみました。

解いてみた

今回は第5問を解いてみたいと思います。

Q5

"echo 響け!ユーフォニアム" からはじめて、次のような出力を得てください。なお、出題者はこのアニメを見たことがありません。

響け!ユーフォニアム
 響け!ユォニアム
  響け!ニアム
   響けアム
    響ム
     
     
    ム響
   ムアけ響
  ムアニ!け響
 ムアニォユ!け響
ムアニォフーユ!け響

・・・未知の事に遭遇すると思考停止することってあると思うんですが、まさにそれでした。

問題考えた人はどうやってこの問題を思いついたのでしょうか。

あと、「響け!ユーフォニアム」は観るべきです。

回答例

【ネタバレ注意】ここから回答例を示した後に解説を進めます。自力で回答してみたい方はスクロールしないでください。

まずは、回答例を示します。

【問題と解答】jus共催 第35回またまためでたいシェル芸勉強会

echo '響け!ユーフォニアム' | \
    awk '{a=$1;for(i=1;i<=6;i++){print substr($1,1,length(a)/2-i+1)substr($1,length(a)/2+i)}}' | \
    pee cat 'rev|tac' | \
    awk '{for(i=1;i<=5-length($0)/2;i++){printf " "}print}'
響け!ユーフォニアム
 響け!ユォニアム
  響け!ニアム
   響けアム
    響ム
     
     
    ム響
   ムアけ響
  ムアニ!け響
 ムアニォユ!け響
ムアニォフーユ!け響

・・・すげぇ(語彙力低下)

それでは(恐れ多いですが)解説を行ってみたいと思います。

問題を理解する

まずは、問題を正しく理解することから始めます。

まず、「響け!ユーフォニアム」という10文字の全角文字列が元ネタです。

その文字列に対して中央の2文字を切り取り、 中央寄せして次の行に表示します。

以降、直前に出力した文字列に対して、文字列がなくなるまで同じ処理(切り取りと中央寄せ)を繰り返しています。

表示する文字がなくなったら、文字列を左右反転させてこれまでに表示された文字列を逆の順番で表示し、 最終的に「ムアニォフーユ!け響」と表示させます。

アプローチを確認

申し上げるまでもないかと思いますが一応述べておきます。

問題を解くにあたり、いきなり結果を求めようとしてはいけません。 段階的に答えに近づけていきましょう。

実際、回答例もパイプで各コマンドの実行結果を順番に処理しています。 ということで、各コマンドの実行結果を確認していきながら出力を回答に近づけていきましょう。

動作環境

なお、動作確認はWindows 10のWSL(ubuntu)上で実施しました。

cat /etc/issue
Ubuntu 16.04.4 LTS \n \l
bash --version
GNU bash, バージョン 4.3.48(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2013 Free Software Foundation, Inc.
ライセンス GPLv3+: GNU GPL バージョン 3 またはそれ以降 <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

中央の2文字を切り取る

まずは、「中央の2文字を切り取る」処理を行っています。

echo '響け!ユーフォニアム' | \
    awk '{a=$1;for(i=1;i<=6;i++){print substr($1,1,length(a)/2-i+1)substr($1,length(a)/2+i)}}'
響け!ユーフォニアム
響け!ユォニアム
響け!ニアム
響けアム
響ム

なぜこのような出力を得ることができるか確認してみましょう。

awk

そもそも、awkとは何でしょうか?

awkはテキストの処理に強みを持つプログラミング言語です。 そのため、先程のようなfor文も使えます。 詳細は私では語れないのでググってください。

AWK - Wikipedia

for文自体についての説明はここでは割愛します。 以下のサイトに制御文に関する説明がありますので必要に応じて参照してください。

IBM Knowledge Center - awk コマンド

awkコマンド(テキストの加工やパターン処理をする) _ JP1_Advanced Shell

まず、変数aに、echo '響け!ユーフォニアム'で出力した結果を格納しています。

echo '響け!ユーフォニアム' | \
    awk '{a=$1;print a}'
響け!ユーフォニアム

ちなみに、$1は現在読み込んでいるレコードの内容を格納している特殊な変数で、$1は1つ目のフィールド、$2は2つ目のフィールド、のように参照できます。

その後、for(i=1;i<=6;i++){print substr($1,1,length(a)/2-i+1)substr($1,length(a)/2+i)}の部分で中央2文字の切り取りと表示(正確には中央の2文字を除いた両端の文字列を連続して表示)を表示する文字がなくなるまで実行しています。

ちなみに、入力文字列が10文字固定であることが前提となっています。 i=1;i<=6となっているのはそのためです。 (「任意の文字数および文字数が奇数だと難易度が上がるので、その前提でOK」と主催者の方が当日おっしゃっていました。)

i=1のとき、print substr($1,1,5)substr($1,6)となり、1文字目から5文字目まで(substr($1,1,5))と6文字目以降(substr($1,6))が連続して表示されます。 (切り取りなし)

echo '響け!ユーフォニアム' | \
    awk '{a=$1;print substr($1,1,5)substr($1,6)}'
響け!ユーフォニアム

ちなみに、substr関数は部分文字列を出力する関数で、第1引数が元ネタの文字列、第2引数が文字列の出力を開始する位置、第3引数(任意)は出力する文字数です。 第3引数が省略された場合、開始位置から文字列の最後までが出力されます。

i=2のとき、print substr($1,1,4)substr($1,7)となり、1文字目から4文字目まで(substr($1,1,4))と7文字目以降(substr($1,7))が連続して表示されます。

echo '響け!ユーフォニアム' | \
    awk '{a=$1;print substr($1,1,4)substr($1,7)}'
響け!ユォニアム

このように、表示する文字の範囲を順次狭くしていき、i=6のときには表示する文字がなくなります。

文字列を反転して出力する

次に、pee cat 'rev|tac'の部分が何をやっているか確認してみましょう。

echo '響け!ユーフォニアム' | \
    awk '{a=$1;for(i=1;i<=6;i++){print substr($1,1,length(a)/2-i+1)substr($1,length(a)/2+i)}}' | \
    pee cat 'rev|tac'
響け!ユーフォニアム
響け!ユォニアム
響け!ニアム
響けアム
響ム


ム響
ムアけ響
ムアニ!け響
ムアニォユ!け響
ムアニォフーユ!け響

rev

revコマンドは入力した文字列を反転して出力してくれます。

echo '響け!ユーフォニアム' | \
    awk '{a=$1;for(i=1;i<=6;i++){print substr($1,1,length(a)/2-i+1)substr($1,length(a)/2+i)}}' | \
    rev
ムアニォフーユ!け響
ムアニォユ!け響
ムアニ!け響
ムアけ響
ム響

tac

tacコマンドは入力を逆順に出力します。

echo '響け!ユーフォニアム' | \
    awk '{a=$1;for(i=1;i<=6;i++){print substr($1,1,length(a)/2-i+1)substr($1,length(a)/2+i)}}' | \
    tac
響ム
響けアム
響け!ニアム
響け!ユォニアム
響け!ユーフォニアム

先程のrevコマンドと連続して利用するとこうなります。

echo '響け!ユーフォニアム' | \
    awk '{a=$1;for(i=1;i<=6;i++){print substr($1,1,length(a)/2-i+1)substr($1,length(a)/2+i)}}' | \
    rev | tac
ム響
ムアけ響
ムアニ!け響
ムアニォユ!け響
ムアニォフーユ!け響

pee

最後に、peeコマンドとは何でしょうか?

peeコマンドはmoreutilsの一部で、指定したコマンドを実行してパイプに出力してくれます。 回答例のように、複数のコマンドを指定してその結果を次のパイプに結合して出力できます。

求めたい出力の前半部分はこれまでの処理結果が利用できそうなので、catで出力します。

求めたい出力の後半部分は、rev | tacで処理した結果が利用できそうです。

peeコマンドを用いて2つのコマンドの結果を次のパイプにまとめて出力します。

echo '響け!ユーフォニアム' | \
    awk '{a=$1;for(i=1;i<=6;i++){print substr($1,1,length(a)/2-i+1)substr($1,length(a)/2+i)}}' | \
    pee cat 'rev|tac'
響け!ユーフォニアム
響け!ユォニアム
響け!ニアム
響けアム
響ム


ム響
ムアけ響
ムアニ!け響
ムアニォユ!け響
ムアニォフーユ!け響

ここまで来るとあと一息ですね(白目)

中央寄せ

最後のawk '{for(i=1;i<=5-length($0)/2;i++){printf " "}print}'の部分を見ていきましょう。

結論から先に述べると、中央寄せと書きましたが、正確には切り取った文字数の半分(5-length($0)/2)の全角空白文字を行の先頭に挿入しています。 ちなみに、$0は入力から現時点で読み込んでいるレコード(行)です。

10文字(length($0)==10)の行であれば、for(i=1;i<=0;i++)となり、{printf " "}が一度も実行されないので全角空白文字は挿入されず、レコードがそのまま出力されます。

echo '響け!ユーフォニアム' | \
    awk '{for(i=1;i<=0;i++){printf " "}print}'
響け!ユーフォニアム

8文字の行であれば、for(i=1;i<=1;i++)となり、{printf " "}が1回だけ実行さるため全角空白文字が1文字だけ挿入され、続けてレコードがそのまま出力されます。

$ echo '響け!ユォニアム' | \
    awk '{for(i=1;i<=1;i++){printf " "}print}'
 響け!ユォニアム
$

for文の繰り返し条件を文字数を利用して適切に定義することで、最終的に期待する結果を得ることができるわけです。 i<=5-length($0)/2と書くとピンとこないかも知れませんが、i<=(10-length($0))/2とすれば、「切り取った文字数(10-length($0))の半分の空白文字を行の先頭に挿入」していることがわかるかと思います。

echo '響け!ユーフォニアム' | \
    awk '{a=$1;for(i=1;i<=6;i++){print substr($1,1,length(a)/2-i+1)substr($1,length(a)/2+i)}}' | \
    pee cat 'rev|tac' | \
    awk '{for(i=1;i<=(10-length($0))/2;i++){printf " "}print}'
響け!ユーフォニアム
 響け!ユォニアム
  響け!ニアム
   響けアム
    響ム
     
     
    ム響
   ムアけ響
  ムアニ!け響
 ムアニォユ!け響
ムアニォフーユ!け響

解説は以上です。お疲れ様でした。

ちなみに、回答はこれ以外にもいろんなパターンが存在します。 twitterでいくつか回答が流れていましたので、参考にしてみるとよいと思います。

まとめ

シェル芸に求められるもの(私に足りなかったもの)

とにかく知らないコマンドばかりでした。この問題では利用していませんが、素因数分解を行うfactorコマンドなどはこの勉強会でその存在を知りました。

factor 1234567890
1234567890: 2 3 3 5 3607 3803

また、awkコマンドも普段使うわけではないので、やりたいことが分かっていてもそれをどう表現していいか全く出てきませんでした。

知識・経験共に圧倒的に足りていませんでした。

シェル芸は苦だが役に立つ

順を追って調べていくと一つ一つの処理は決して難しいものではなく、単純な処理の積み上げであることがわかります。 ただ、いざ問題が問題を目の前にするとどうやって道筋を立てればいいのか、途方に暮れてしまいます。 こればかりは慣れるしかないんだと思います。 それまでは修行あるのみです。

ただ、シェル芸自体はとても楽しく、またその中で利用しているテクニックはきっと業務でも役に立つはずです。 特にawkはおさえておいて損はないでしょう。

シェル芸に興味を持った方は、シェル芸勉強会に参加してみてはいかがでしょうか?