第32回シェル芸勉強会 Q2 の解答

前置き

昨日に続いて第32回シェル芸勉強会のQ2を解いていきます。

第32回シェル芸勉強会の問題は次のURLにあります。
【問題のみ】jus共催 第32回全くインスタ映えしないシェル芸勉強会 | 上田ブログ

思考を忠実にアウトプットすることを目標にしています。

Q2 問題(上記URLからコピー)

Q1と同じ入力から始めて、今度は

1
a
b
4
c
6
7
d
9

というように、間をa,b,c,...と埋めてください。

問題を見た感想・補足

Q1の続きでした。。。下記部分はQ1の答えをそのまま使っています。

echo 14679 | grep -o . | diff -u <(seq 9) - | sed 1,3d | sed '/^-/s/.*//;s#.##'

解答1

echo 14679 | grep -o . | diff -u <(seq 9) - | sed 1,3d | sed '/^-/s/.*//;s#.##' | awk '/^$/{print "a "++i}!/^$/{print}' | sed '/^a/y/1234/abcd/' | awk '$0=$NF'

解説

間の空白を補完するだけなので簡単だろうと見てすぐは思いましたが、補完している値がアルファベットのため話がめんどくさくなっています。

まず、加工すべき行とそのまま出力すべき行があるので、それを分別する必要があります。
少し冗長ですが、awk 'pattern{加工処理} !pattern{print}'とすれば、pattern に一致するもののみを加工できます。今回は空白行のみを加工したいので、pattern/^$/または!lengthです。
加工した結果で出力する内容は、次にしたい処理を考えてからでないと構築できないので少しあとで解説します。

今回の出力で追加する情報にのみ注目すると、1つずつ変わるアルファベットなので、普段であれば連番をアルファベットに変換する tr 1-9 a-i を使いたくなります。
ただし今回は、そのまま出力したい行にも数字が含まれているのでtrを使ってしまうと、一緒くたに変換されてしまって困ります。
sedy命令を使えば、trと同様のことができる上に、条件に合う行だけを扱うことができるので、加工したい行に条件を追加できれば今回は都合がいいです。

加工したい行を出力するのは、先程後回しにしたawkです。awkであれば、加工対象行にタグを付けるといったことは朝飯前なので、そのようにします。(以下では行頭に"a"の列を追加しています。)

$ echo 14679 | grep -o . | diff -u <(seq 9) - | sed 1,3d | sed '/^-/s/.*//;s#.##' | awk '/^$/{print "a "++i}!/^$/{print}'
1
a 1
a 2
4
a 3
6
7
a 4
9

タグを目標に sed します。
>|bash||
$ echo 14679 | grep -o . | diff -u <(seq 9) - | sed 1,3d | sed '/^-/s/.*//;s#.##' | awk '/^$/{print "a "++i}!/^$/{print}' | sed '/^a/y/1234/abcd/'
1
a a
a b
4
a c
6
7
a d
9
|

後はタグを消すawk '$0=$NF'を付ければ解答1になります。

解答2

echo 14679 | grep -o . | diff -u <(seq 9) - | sed 1,3d | sed '/^-/s/.*//;s#.##' | awk '/^$/{print "9+"++i}!/^$/{print}' | cat <(echo obase=16) - | bc | tr A-F a-f

解説

追加する行がアルファベットなので困ると、解答1の解説には書きましたが、出てくるアルファベットが d までなので、16進数の範囲で数値として扱うことができます。
なのでn進数計算ができる計算機である bc を使えば加算を使うことができます。
awkbc の計算式を作って計算しているだけです。(何のオプションか毎回忘れるのですが、オプション無しだと大文字で出力されるので trで小文字に変換しています。)

解答3

echo 14679 | grep -o . | diff -u <(seq 9) - | sed 1,3d | sed '/^-/s/.*//;s#.##' | tr 1-9 a-i | awk '/^$/{$0=++i}{print}' | tr 1-9a-i a-i1-9

解説

「空白行に数字を出力し、その後に目的のアルファベットに変換すると、そのまま出力すべき行にも影響してしまう」ことが問題でした。
なので、一旦数字以外のものに変換してから元に戻してやれば影響はありません。

解答4

echo 14679 | grep -o . | diff -u <(seq 9) - | sed 1,3d | sed '/^-/s/.*//;s#.##' | sed '/^$/!s#^#echo #' | sed '/^$/s##i=$((i+1));echo "obase=16;9+"$i | bc#' | sh | tr A-Z a-z

解説

sh に投げればなんでもできる。
awk を使うとなんとなく負けた気分になるので、なくしました。
sed でコマンドを作って、最終的に sh に投げる」というとてもやりがちな方法です。

今回の、そのまま出力したい行は前に echoを付けると概ね解決できます。(今回はありませんが、行中に$などのメタ文字が含まれる場合は適切にエスケープする必要があります。)
以下のワンライナーsh に渡すと、そのまま出力したい行のみを出力します。

$ echo 14679 | grep -o . | diff -u <(seq 9) - | sed 1,3d | sed '/^-/s/.*//;s#.##' | sed '/^$/!s#^#echo #'
echo 1


echo 4

echo 6
echo 7

echo 9

あとは解答1, 2, 3のいずれかのように加工すれば望みのものが得られます。(今回は解答2の16進数計算を使用)

$ echo 14679 | grep -o . | diff -u <(seq 9) - | sed 1,3d | sed '/^-/s/.*//;s#.##' | sed '/^$/!s#^#echo #' | sed '/^$/s##i=$((i+1));echo "obase=16;9+"$i | bc#'
echo 1
i=$((i+1));echo "obase=16;9+"$i | bc
i=$((i+1));echo "obase=16;9+"$i | bc
echo 4
i=$((i+1));echo "obase=16;9+"$i | bc
echo 6
echo 7
i=$((i+1));echo "obase=16;9+"$i | bc
echo 9

補足

bcshの順番が入れ替わっても同様に動作するので、次のワンライナーも解答になります。

echo 14679 | grep -o . | diff -u <(seq 9) - | sed 1,3d | sed '/^-/s/.*//;s#.##' | sed '/^$/!s#^#echo #' | sed '/^$/s##i=$((i+1));echo "obase=16;9+"$i#' | sh | bc | tr A-Z a-z

まとめ

次の2つのことが課題でした。

  • 加工したい行と加工したくない行のあつかい
  • アルファベットの連番(ちょっと微妙な表現ですが)

感想

いかにしてアルファベットを数値として扱ってやるかを考えるとなんとかなりました。
xxd でバイナリに変換して計算してもできそうですね。


改訂履歴

  • 2017/12/08: 「まとめ」を「感想」にして、きちんとした「まとめ」を書きました。