LoginSignup
11
11

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-11-22

問題

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

標準入力から与えられるテキストデータで、見出し行(インデント無しで大文字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有ったり無かったり>

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

11
11
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11