問題
この問題、解けるだろうか?
標準入力から与えられるテキストデータで、見出し行(インデント無しで大文字1単語だけの行と定義)を除去するフィルターを作れ。
ただし、それ以外の変化は一切させないこと。 例えば入力テキストデータの最後が改行で終わっていない場合は、出力テキストデータも改行で終わらせてはならない。
問題文の前半だけなら簡単だ。grepコマンド1個で簡単に書けてしまう。
$ printf 'PROLOGUE\nA long time ago...\n' | grep -v '^[A-Z]\{1,\}$'
A long time ago...
$
だがテキストが改行コードで終わってない場合は、加工後の文字列にも勝手に改行コードを付けてはいけない。つまり次のような動きだ。
$ printf 'PROLOGUE\nA long time ago...\n' | ANSWER
A long time ago...
$
$ printf 'PROLOGUE\nA long time ago...' | ANSWER
A long time ago...$
テキストを出力する時、多くのUNIXコマンドでは行末に自動的に改行コードが付加されるので一筋縄ではいかないというわけだ。
さて、どうやるか。
Thinking time
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
:
こたえ
(cat -; echo) |
grep -v'^[A-Z]\{1,\}$' |
awk 'BEGIN{
ORS="";
OFS="";
getline line;
print line;
dlm=sprintf("\n");
while (getline line) {
print dlm,line;
}
}'
つまり、加工をするコマンド(この例ではgrep -v'^[A-Z]\{1,\}$'
)の前に
(cat -; echo)
をパイプで挿み、加工をするコマンドの後に、
awk '
BEGIN{
ORS="";
OFS="";
getline line;
print line;
dlm=sprintf("\n");
while (getline line) {
print dlm,line;
}
}
'
をパイプ越しに追加すればよい。
ただしsedは例外
もし見出し行を除去するフィルターをsedコマンドでsed '/^[A-Z]\{1,\}$/d'
と思いついていたとして、それを活用するのならこう答えるのが無難だ。
(cat -; echo) |
sed '/^[A-Z]\{1,\}$/d' |
grep '^' | # (結局grep使っているけど)これが更に必要
awk 'BEGIN{
ORS="";
OFS="";
getline line;
print line;
dlm=sprintf("\n");
while (getline line) {
print dlm,line;
}
}'
GNU版のsedは、改行無し終端テキストに改行コードを付けずに出力することがわかった。テキストの最後に改行コードを付け足すsed実装も存在する中で、どちらでも動くようにするには、sedの後ろにgrep '^'
などと、必ずテキスト終端に改行を付け足すコマンドを挿んで、挙動を揃える必要がある。
その他、改行コードを付けないフィルターコマンド
cat、dd、head、tail、trなど。
これらは、元々改行コードを付けないコマンドなので本Tipsのような細工は不要だ。
解説
多くのUNIXコマンドは、改行が無いと途中のコマンドが勝手に改行を付けてしまうが、それは困る。そこで先手を討って先に改行を付けてしまう。そうすると、途中に通すコマンドが勝手に改行を付けることは無くなる。そして最後に末端の改行を取り除けばいいというわけだ。
というわけで、前後に追加したコマンドはそれぞれ、元のテキストデータの最後に改行コードを1つ付加し、最後にそれを取り除くということをやっている。最初の付加をしているコードは分かりやすいだろうが、最後の除去をしているコードはどうやっているのか。
AWKコマンドの性質を1つ利用している。AWKコマンドは、printfで改行記号\n
を付けなかったり、組み込み変数ORS(出力レコード区切り文字)を空にしたりすれば行末に改行コードを付けずにテキストを出力できる。後ろに追加したAWKはこの性質を利用し、普段なら改行コードを出力した時点で行ループを区切るところを、行文字列を出力して改行コードを出力する手前で行ループを一区切りさせるようにしてしまう。
そうすると一番最後の行のループだけは不完全になり、最後の行の文字列の後ろに改行コードが付かないことになる。
しかし、予め余分に改行を1個(つまり余分な1行)を付けておいたので、不完全になるのはその余分な1行ということになる。結果、元データの末端に改行が含まれていなければ末端には改行が付かないし、あれば付く。
言葉では分かり難いかもしれないが、図で解説するとこんな感じだ。
str_1<LF>
str_2<LF>
:
str_n<LF有ったり無かったり>
↓(末端に改行コードを付加)
str_1<LF>
str_2<LF>
:
str_n<LF有ったり無かったり><LF>
↓(加工する。各行末に必ず改行コードがあるので、勝手に付加されない)
STR_1<LF>
STR_2<LF>
:
STR_n<LF有ったり無かったり><LF>
↓(各行末の改行コードが次行の行頭に移動したように扱う)
STR_1
<LF>STR_2
:
<LF>STR_n<LF有ったり無かったり>
<LF>
↓(最終行の改行をトル)
STR_1
<LF>STR_2
:
<LF>STR_n<LF有ったり無かったり>
||(これってつまり……)
STR_1<LF>
STR_2<LF>
:
STR_n<LF有ったり無かったり>
ちょっと不思議な気もするが、そういうことだ。