シェルにおけるタブと改行の扱い

最近はradikoらじる★らじるのタイマー録音絡みで、シェルスクリプトを触る機会が多かったのだが、

自分の思いどおりに動作する時とうまく動かない時の原因は、そのほとんどが、スペース・タブ・改行を扱う時の勘違いにあると感じてしまった...。その扱いを正確に理解していれば、シェルスクリプトはかなり高度なテキスト処理を最初から的確にこなしてくれるはずだ。シェルスクリプトを理解するポイントの1つは、これらの目に見えない文字を、頭の中で正確に思い描く想像力かもしれない。現状の理解をメモしてみた。

検証環境

$ bash --version
GNU bash, version 3.2.48(1)-release (x86_64-apple-darwin12)
Copyright (C) 2007 Free Software Foundation, Inc.

echoコマンド

改行の扱い
hello
world
  • NG例
$ echo hello\nworld
hellonworld

$ echo "hello\nworld"
$ echo 'hello\nworld'
hello\nworld
  • 正解=eオプションを指定する
$ echo -e "hello\nworld"
$ echo -e 'hello\nworld'
hello
world
  • 但し、eオプションを指定してもクォートしておかないと...
$ echo -e hello\nworld
hellonworld
  • 原因は、echoコマンドへ引数がわたる前に、先にシェルでバックスラッシュが評価されてしまうためらしい。
  • これを回避するためには、バックスラッシュをさらにエスケープすればOK。
$ echo -e hello\\nworld
hello
world
  • さらに、eオプションを利用しないで2行書きしてみる。
$ echo hello$'\n'world
$ echo $'hello\nworld'
hello
world
  • まとめ
$ echo -e "hello\nworld"
$ echo -e 'hello\nworld'
$ echo -e hello\\nworld
$ echo hello$'\n'world
$ echo $'hello\nworld'
hello
world
タブの扱い
  • 以上の記法は、タブにおいてもすべて有効である。
$ echo -e "hello\tworld"
$ echo -e 'hello\tworld'
$ echo -e hello\\tworld
$ echo hello$'\t'world
$ echo $'hello\tworld'
hello   world
直接入力
  • ワンライナーにこだわらなければ、改行を直接入力してしまってもOK。
$ echo 'hello
    
hello world
  • では、タブを直接入力する方法はあるのだろうか?
  • tabを押しても警告音が鳴って入力できないのだ...。
  • 原因はtabにはコマンド補完の機能が割り当てられているので、補完入力の候補なしという意味で警告音が鳴っているのだ。
  • このコマンド補完を回避するには「control-V、tab」このキー操作でタブが入力できた。
$ echo 'hello       world'
hello   world

変数展開

  • 変数に改行を含む文字列が設定されている場合も注意が必要。
str='hello
world'
  • クォートしない変数展開では、改行はスペースになってしまう。
$ echo $str
hello world
  • クォートした変数展開は、ちゃんと改行として出力される。
$ echo "$str"
hello
world
  • この違いは、grepなどパイプで連携させた場合に悩ましいエラーの原因になる。
$ echo $str|grep hello
hello world

$ echo "$str"|grep hello
hello

\nは改行とは違う

  • また、\n = 改行ではなく、あくまでもエスケープ記法を使った単なる\nという文字列である。
str='hello
world'

$ echo "$str"
hello
world
str='hello\nworld'

$ echo "$str"
hello\nworld

$ echo -e "$str"
hello
world
  • 区切り文字を使ったループ処理でも同じ。
  • \nでは1回しかループしていないが、
str='hello\nworld'

$ for s in $str; do echo " -- $s --"; done
 -- hello\nworld --
  • 改行ならちゃんと2回ループしている。
str='hello                       
world'

$ for s in $str; do echo " -- $s --"; done
 -- hello --
 -- world --

改行を置き換える

  • 稀に「改行」を別の文字列に置き換えたくなることがある。
  • 置き換えと言えばsedコマンドなのだが、改行を目印に、行単位で処理するsedコマンドは、改行の置き換えが苦手...。
  • それでもsedにはいろいろな技があるのかもしれないが...
  • 無理してsedコマンドを使い続けるよりも、素直に別の方法で改行を置き換えた方がシンプルで分かり易くなるのだ。
sedコマンド
str='hello
world'
  • sedは\nを認識してくれないので、何も置き換えが起こらない...。
$ echo -n "$str"|sed 's/\n/,/'
hello
world
  • かろうじて、改行以外の文字を改行へ置き換えることは可能である。
$ echo -n "$str"|sed 's/l/\
    
he lo wor d
trコマンド
  • 1文字限定だが、trコマンドなら\nを認識してくれるので、素直に改行を置き換えできる。
$ echo -n "$str"|tr '\n' ','
hello,world
  • しかし、trは1対1の置き換えなので、複数文字列の__,__に置き換えたい、なんて場合はお手上げ...。
Perl
  • それがPerlなら、ほぼsedコマンドと同じ書式で当然のように改行の置き換えができてしまう。
$ echo -n "$str"|perl -pe 's/\n/__,__/' $stdin
hello__,__world
  • シンプル、かつ分かり易い書式である。素晴らしい!
Ruby
  • 同様にRubyでも可能。gsubというRuby独自のコマンド書式だけど、ちゃんと置き換えできる。
$ echo -n "$str"|ruby -pe 'gsub(/\n/, "__,__")' $stdin
hello__,__world
awk
  • awkコマンドを使うのもありかもしれないが、最後の区切り文字を取り除きたい時の方法が分からない...。
$ echo -n "$str"|awk -F '\n' -v ORS='__,__' '{print}'
hello__,__world__,__

タブを置き換える

  • タブの置き換えについては、sedでも普通にできる。
  • 但し、sedではエスケープ記法の\tが使えないので、control-Vとtabで直接タブ入力してみた。
str='hello    world'
$ echo "$str"|sed "s/   /__,__/"
hello__,__world
  • tr、PerlRubyawkについては、\nの部分を\tに変更すれば、同じように置き換えできた。

タブ幅の設定

  • OSXbash環境では、デフォルトのタブ幅は8。
  • この設定は、tabsコマンドで変更できる。
$ echo "hello   world"
hello	  world

$ tabs -6

$ echo "hello   world"
hello	world

タブとスペースの変換

  • タブは便利だけど、環境によってタブ幅が変化してしまうと、書式がズレたりして不都合が起こるかもしれない。
  • そこで、タブをスペースに変換したり、逆にスペースをタブに変換しながら、データを処理することがよくある。
expandコマンド
  • tabsコマンドの設定に影響されず、デフォルトでタブ幅8の環境としてタブをスペースに変換する。
$ tabs -6

$ echo "hello   world"
hello world

$ echo "hello   world"|expand
hello   world
  • オプションでタブ幅を指定すれば、指定したタブ幅の環境としてタブをスペースに変換する。
$ echo "hello   world"|expand -16
hello           world
unexpandコマンド
  • デフォルトでタブ幅8の環境の出力結果として、スペースをタブに変換する。
  • タブ幅と不一致なスペース区切りは、そのままスペースとして出力される。
  • 例:hello_ _ _ _ _world(5つのスペース)を、タブ幅8で、タブ幅8の環境にunexpandする場合。
$ tabs -8
$ echo "hello     world"|unexpand -a
hello	  world
  • 出力:hello \t_ _world(1つのタブと、2つのスペース)
    • hello+スペース3つの部分(8文字分)がタブに置き換わる。
    • それ以降のタブ区切りは存在しないので、残りスペース2つはそのまま。
  • 例:hello_ _ _ _ _world(5つのスペース)を、タブ幅10で、タブ幅8の環境にunexpandする場合。
$ tabs -8
$ echo "hello     world"|unexpand -a -t10
hello	world
  • 出力:hello \t world(1つのタブ)
    • hello+スペース5つの部分(10文字分)がタブに置き換わる。
sedコマンドでタブ変換
  • 連続するスペースを(その個数に関係なく)とにかく1つのタブに変換したい場合は、sedコマンドを使った方が良さそう。
$ tabs -8
$ echo "hello     world"|sed 's/ \{1,\}/     /'
hello	world

区切り文字との合わせ技

  • シェルのデフォルトの区切り文字は、スペースタブ改行である。
  • 区切り文字の設定は、IFS変数に設定されている。
  • 試しに出力してみると、以下のように出力される。
$ echo -n "$IFS"|ruby -e 'p STDIN.bytes.to_a'
[32, 9, 10]
  • asciiコード32=0x20=スペース、asciiコード9=0x09=タブ、asciiコード10=0x0a=改行LF、なのである。
  • そして、時にこのIFSの設定をIFS=$'\n'などに変更しながらデータ処理を行うことで、2次元の配列としてテキストのデータベースを扱うような処理が可能になるのだ!
  • テキストの中から必要な部分を抜き出すシェルスクリプトのコードは、まるで暗号のようだけど...
  • 正規表現と組み合わせたその表現力は、シンプルな1行に凝縮された処理を詰め込むことができる。
  • AppleScriptで書いたら何十行にもわたる処理が、たった1行で表現できてしまうシンプルさは、素晴らしいと感じた。(コード全体の見通しが、素晴らしく良くなるのだ)