ShellScript
awk
UNIX
shell

改行無し終端テキストを扱う

More than 3 years have passed since last update.


問題

この問題、解けるだろうか?


標準入力から与えられるテキストデータで、見出し行(インデント無しで大文字1単語だけの行と定義)を除去するフィルターを作れ。

ただし、それ以外の変化は一切させないこと。 例えば入力テキストデータの最後が改行で終わっていない場合は、出力テキストデータも改行で終わらせてはならない。


問題文の前半だけなら簡単だ。grepコマンド1個で簡単に書けてしまう。


grep一発でできる

$ printf 'PROLOGUE\nA long time ago...\n' | grep -v '^[A-Z]\{1,\}$'

A long time ago...
$

だがテキストが改行コードで終わってない場合は、加工後の文字列にも勝手に改行コードを付けてはいけない。つまり次のような動きだ。


こういう動きをするをANSWER部分のコード作れというのがこの問題

$ 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有ったり無かったり>

ちょっと不思議な気もするが、そういうことだ。