問題が起こった所
シェルスクリプトの中で文字列を扱っている時に、 それを出力するにあたって末尾に空白行とかがある時にそれを無視 したい時がありました。
この辺でごちゃごちゃやってる時のこと。
シェルスクリプトの中で、文字列の行数を数えてその分だけ空きを作る事。
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*$//'
で、行末の改行コードを全て消しています。
次に、sed
でN
を使って行を繋げて行きますが、この際、
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に以下の様に設定しておきます。
1 2 3 |
|
新しいオプションなので、古い環境との互換性を保たせるためにバージョンを見て 十分ならばこのオプションを設定するようにしています。
sed ファイル内の改行を削除, 連続した空白を1つにする - Qiita: http://qiita.com/hidekuro/items/56acc773db777100bf09↩-
上の中で分けるのに必要なのは
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, '}' -
これはバグ? ↩