rcmdnk's blog

sed: sed-Politicus, Sozialistische Einheitspartei Deutschlands, Erich Honecker, Wolfgang Leonhard, Hans Mahle, Lutz Heilman

文字列の最後に余計な空白行とかが付いてる時に消す事を sedを使ってやろうと思ったら結構大変だった件。

問題が起こった所

シェルスクリプトの中で文字列を扱っている時に、 それを出力するにあたって末尾に空白行とかがある時にそれを無視 したい時がありました。

Issue #4 · rcmdnk/sentaku

この辺でごちゃごちゃやってる時のこと。

シェルスクリプトの中で、文字列の行数を数えてその分だけ空きを作る事。 tputとかでカーソル位置を直接動かすので行数を自分で数えておく必要があります。

この際に、適当に作ってた時に文字列の最後に改行を入れることが前提になってて、 文字列によって実際の表示が崩れたりしたのでそれを直した話です。

所謂、エディタが勝手にテキストファイルの最後に入れた改行を削除したい、 という話とはちょっと別の話です。

sedでの改行の消し方

sedコマンドは1行ずつ読み込んで操作を行うため、 改行の様な複数の行をまたぐ時には少し工夫が必要です。

改行を消すのによくやるのが

$ printf "$value" | sed ':loop; N; $!b loop; s/\n//g'

こんな感じのコマンド 1

sed;で区切ることで複数のコマンドを一緒に書くことが出来るので、 上でやってることは

  • :loop: : + val、でこの場所にvalというラベルを設定(loopという文字列は何でも良い)。
  • N: 次の行を現在の行に繋げる。(この際、2つの行の間には改行コード(\n)が入る。)
  • $!b loop: b loopがラベルloopへ飛ぶ、と言うgotoコマンド。 コマンドの前には条件を書くことが出来て、$は読み込んでる物の一番最後の行である、と言う条件、 さらに!は条件の否定、なので、ここでは、最終行でなければloopに戻る、 という意味になります。結果的に最初の行から最後の行までを改行コードを挟んで繋げる事になります。
  • s/\n//g: よく使う置換コマンド。\nが改行で、置換後には何も無く、 さらにgを最後に指定してるので全ての改行コードを消すコマンド。

と言った感じです。 Nで行をつなげると、改行コードを含む一列として扱える様になるので、 すべての行をつなげた後、全体に対して置換をしています。

ただ、GNUのsedだとこれで大体上手く行くのですが、MacなんかのBSD sedだと上手くいきません。 BSDの場合には

$ printf "$value\n\n" | sed -e :loop -e 'N; $!b loop' -e 's/\n//g'

の様に各コマンドを分けて書く必要があり2、 さらに文字列が一行だけの時や、一行+改行コード、だけの場合 これを通すと消えてしまうので、それを避けるために少なくとも 2つ余計に改行コードを加えて置く必要があります3

分ける分にはGNU sedでも問題なく動くので、BSDでも動くようにするために -eを使って分けて書いておいた方が互換性が良いです。

また、改行コードを加える点も両者で同じ振る舞いにするには必要です。

この最後の改行コードの部分がまた微妙に違う所があって、 BSD sedだと元のインプットの最後が改行コードであろうと何であろうと 必ず最後に改行コードを加えるのですが、 GNU sedだと元のインプットにあるかないかに依存します。

まとめてみると

  • BSD sed
    • 一行だけ(行の最後に改行だけある場合も含む)の場合は何も出力されなくなる。

      $ value="a"
      $ printf "$value"|sed -e :loop -e 'N; $!b loop' -e 's/\n//g'
      $ # Nothing!
      $ value="a
      > "
      $ printf "$value"|sed -e :loop -e 'N; $!b loop' -e 's/\n//g'
      $ # Nothing, too!
      
    • 最後に必ず改行コードが加えられる。

      $ value="a
      > b"
      $ printf "$value"| od -c
      0000000    a  \n   b
      0000003
      $ printf "$value"|sed -e :loop -e 'N; $!b loop' -e 's/\n//g'|od -c
      0000000    a   b  \n
      0000003
      $ value="a
      > b
      > "
      $ printf "$value"|od -c
      0000000    a  \n   b  \n
      0000004
      $ printf "$value"|sed -e :loop -e 'N; $!b loop' -e 's/\n//g'|od -c
      0000000    a   b  \n
      0000003
      
  • GNU sed
    • 一行だけでも出力される。

      $ value="a"
      $ printf "$value"|sed -e :loop -e 'N; $!b loop' -e 's/\n//g'
      a$
      $ value="a
      > "
      $ printf "$value"|sed -e :loop -e 'N; $!b loop' -e 's/\n//g'
      a
      $
      
    • インプットに改行コードがあるかどうかで、最後に改行コードが付くかどうかが決まる。

      $ value="a
      > b"
      $ printf "$value"| od -c
      0000000    a  \n   b
      0000003
      $ printf "$value"|sed -e :loop -e 'N; $!b loop' -e 's/\n//g'|od -c
      0000000   a   b
      0000002
      $ value="a
      > b
      > "
      $ printf "$value"|od -c
      0000000    a  \n   b  \n
      0000004
      $ printf "$value"|sed -e :loop -e 'N; $!b loop' -e 's/\n//g'|od -c
      0000000    a   b  \n
      0000003
      $ value="a
      > b
      >
      > "
      $ printf "$value"|od -c
      0000000   a  \n   b  \n  \n
      0000005
      $ printf "$value"|sed -e :loop -e 'N; $!b loop' -e 's/\n//g'|od -c
      0000000   a   b  \n
      0000003
      

こんな感じです。

結構色々気をつけないといけないのと、 BSDの場合とGNUでインプットの最後に改行がある場合は 必ず最後に改行が1つ付きます。

なので単に改行を消したいだけならtrとか 4 を使ったほうが良いです。

$ value="a
b
"
$ printf "$value"|tr -d '\n'|od -c
0000000    a   b
0000002
$

これだと最後の改行コードも含めて全ての改行コードを消せます。

sedで末尾の空白行を消す

sedの改行コードを消す方法がわかったので、 これを使って末尾の空白行だけを消す事をやってみます。

最終的な形はこんな感じ。

$ printf "%b\n\n" "$value"|sed -e :loop -e 'N; $!b loop' -e 's/[[:space:]\n]*$//'

sedコマンドに関して、最後の置換えのコマンドを

s/[[:space:]\n]*$//

としています。

[]を使うとこの内のどれか、のorが作れ、[:space:]は空白文字、 $はここでは行末です。 *が直前文字の0個以上、なので末尾からみて、空白と改行コードが続く限り消されます。

また、上に書いた様に、BSDだと1行だけだと消えてしまったりするので 2つ改行コードをvalueに加えてからsedに渡しています。 (1つだけ渡すと、value=aのように一行かつ改行コードを含まないものに対しては まだ出力が無くなる。)

改行コードを必ず渡すので、GNU sedの場合にも最後にかならず改行コードが 付くことになります。 これも行数を数える際に重要になってきます。

wcを使って行数を測ってみようとすると、 wcのマニュアルにも

NAME
wc - print newline, word, and byte counts for each file

とあるように 実際には改行の数を測る事になります。

なので、

$ value="a"
$ printf "$a"|wc -l
0

と、なり、GNUの場合に最後に改行コードが無いと期待しているものより一行少なくなります。

なので最後の改行コードについて揃えておくことは重要。

これで、

$ lines=$(printf "%b\n\n" "$value"|sed -e :loop -e 'N; $!b loop' -e 's/[[:space:]\n]*$//'|wc -l)

とでもしておけば末尾空白を除いた行数を得ることが出来ます。

sedで末尾の空白行を消す別の方法

最初、GNUのsedを使った時、[:space:]の代わりに空白をそのまま使って、

$ printf "%b\n\n" "$value"|sed -e :loop -e 'N; $!b loop'  -e 's/[ \n]*$//'

としても上手く行きました。

ただ、これをBSDで試してみると上手く行きませんでした。

空白の代わりに\sと通常空白を表現する正規表現はGNUでもBSDでも効きません。 (GNUの-rとかBSDの-Eなど拡張正規表現を使うようなオプションを使ってもダメでした。)

そこで、[:space:]をする代わりに、最初はこんな感じのを作っていました。

$ printf "%b\n\n" "$value"|sed -e :loop -e 'N; s/ *$//; $!b loop'  -e 's/\n*$//'

まず、最後から見ていくと、 's/\n*$//'で、行末の改行コードを全て消しています。

次に、sedNを使って行を繋げて行きますが、この際、 s/ *$//を挟んで行末の空白を消しています。 これは、最後に\n*$を使って消そうとするとき、 途中に空白文字があるとこれから外れてしまうのを避けるためです。

これだと行の末尾にある空白が全て削除されてしまいますが、 末尾の空白文字だけある所が全部消えて改行だけになるので、 最後の改行だけの削除で消えてくれます。

行末空白を消しても構わない、むしろ要らない、 といった場合にはこんな方法もありかと。

sedで改行を出力する

今回はsedでインプット側の改行コードを操作する話でしたが、 逆に改行コードを出力しようとするときにも GNU/BSDで違いがあったりして結構面倒なので注意です。

ファイル末尾の改行について

今回の話とは別の話になりますが、 ファイル末尾の改行についてもついでに色々見たのでまとめておきます。

テキストファイルを作って普通に保存すると、 大概のエディタなどでは末尾に自動的に改行コードを加えます。

これは、改行コードを行の区切り(separator)と考えるか、 行の終わり(terminator)と考えるか、で末尾に必ず付け加える必要性が 変わってきますが、 POSIX的な考え方ではterminatorとして考えるので 5、 これと同じ考え方のエディタでは保存時に最終行には最後に改行コードを加える様になっています。

上のwcのケースでも行数は改行コードの数を数える事になるので、 最後の行に改行コードが無いと直感的に違った物が出てきてしまいます。

gcc等でも改行が無いファイルを使おうとすると警告が出たりもします 6

この辺り、場合によってはきちんとチェックする必要も出てきます 7

Vimでも通常ファイルを開いて保存する際、 最後に改行コードが無ければ自動的に改行コードが付け加えられます。

これを避けるためには、以前は

: set binary noeol

と、バイナリの設定を行って、さらにeol(endofline)を無効にする方法が 良く出てくるものでした。

eolはまさに最後に改行を加えるかどうか、と言うオプションですが、 noeolとしてもバイナリ出ない限りは必ず改行を加える様になっています。

なので、set binaryをした上でset noeolとする必要があります。

ただし、このset binaryによって通常のテキストモードでは 良しなにVimがしてくれてたエンコード等を上手く扱えなくなるので 色々と不具合が出ることがあります。

と、そこで最近新しくfixeolというオプション 7.4のPatch 7.4.785 から入り、この問題を解決できる様になりました 8

Vimのマニュアルのeolの部分には、7.4.785以降が入っていれば

                        'endofline' 'eol' 'noendofline' 'noeol'
'endofline' 'eol'       boolean (default on)
                        local to buffer
                        {not in Vi}
        When writing a file and this option is off and the 'binary' option
        is on, or 'fixeol' option is off, no <EOL> will be written for the
        last line in the file.  This option is automatically set or reset when
        starting to edit a new file, depending on whether file has an <EOL>
        for the last line in the file.  Normally you don't have to set or
        reset this option.
        When 'binary' is off and 'fixeol' is on the value is not used when
        writing the file.  When 'binary' is on or 'fixeol' is off it is used
        to remember the presence of a <EOL> for the last line in the file, so
        that when you write the file the situation from the original file can
        be kept.  But you can change it if you want to.

の様に書いてあります。

fixeolはデフォルトでは有効ですが、set nofixeolをしておけば、 通常のテキストモードでも改行コードを勝手に付け加えることはなくなります。

これを常に無効にしたい場合、.vimrcに以下の様に設定しておきます。

.vimrc
1
2
3
if (v:version == 704 && has("patch785")) || v:version >= 705
  set nofixeol
endif

新しいオプションなので、古い環境との互換性を保たせるためにバージョンを見て 十分ならばこのオプションを設定するようにしています。

Sponsored Links
  1. sed ファイル内の改行を削除, 連続した空白を1つにする - Qiita: http://qiita.com/hidekuro/items/56acc773db777100bf09

  2. 上の中で分けるのに必要なのはloopのラベルを付けるところと使う所。 これらの後に;を付けてつなげようとすると、ラベルがこれ以降も含めたもの、 として認識しようとするので、予期してないラベル(loop; ...)が付いたり、 ` undefined label ‘loop ; s/\n//g’`と、ラベルは無い、と言ったエラーが出たりします。

    However, for non-GNU sed, some commands require separate expressions on the command line. These include:

    * all labels (':a', ':more', etc.)
    * all branching instructions ('b', 't')
    * commands to read and write files ('r' and 'w')
    * any closing brace, '}'

    OS Xのsedで改行を置換する: kanzメモ

  3. これはバグ?

  4. 改行を置換したい skmks

    シェルにおけるタブと改行の扱い - ザリガニが見ていた…。

  5. Text File / Line - odz buffer

  6. なぜ gcc はファイルの最後に改行がないと警告を出すのか? - Schi Heil と叫ぶために

  7. ファイルの末が改行 LF であるか判定する Bash スクリプト - Qiita

  8. 最後に改行がないファイルが作れない · Issue #152 · vim-jp/issues

Sponsored Links

« VimのNeoBundleのアップデートで出た'Unknown function'のエラーを直す(NeoBundleLazyをちゃんと考えて使う) Macのアイコン作成アプリのImg2icnsが名前が変わってImage2iconになってた »

}