シェルにおけるタブと改行の扱い
最近はradikoやらじる★らじるのタイマー録音絡みで、シェルスクリプトを触る機会が多かったのだが、
- 再びradikoで録音したい! - ザリガニが見ていた...。
- 予約日時になったらちゃんと目覚めるtimerコマンドが欲しい - ザリガニが見ていた...。
- radikoをキーワードで予約する - ザリガニが見ていた...。
自分の思いどおりに動作する時とうまく動かない時の原因は、そのほとんどが、スペース・タブ・改行を扱う時の勘違いにあると感じてしまった...。その扱いを正確に理解していれば、シェルスクリプトはかなり高度なテキスト処理を最初から的確にこなしてくれるはずだ。シェルスクリプトを理解するポイントの1つは、これらの目に見えない文字を、頭の中で正確に思い描く想像力かもしれない。現状の理解をメモしてみた。
検証環境
- MacBook Pro Retina15 OSX 10.8.2
- bash
$ bash --version GNU bash, version 3.2.48(1)-release (x86_64-apple-darwin12) Copyright (C) 2007 Free Software Foundation, Inc.
echoコマンド
改行の扱い
- hello worldを2行書きで出力してみる。
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 'hellohello 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 dtrコマンド
- 1文字限定だが、trコマンドなら\nを認識してくれるので、素直に改行を置き換えできる。
$ echo -n "$str"|tr '\n' ',' hello,world
- しかし、trは1対1の置き換えなので、複数文字列の__,__に置き換えたい、なんて場合はお手上げ...。
awk
- awkコマンドを使うのもありかもしれないが、最後の区切り文字を取り除きたい時の方法が分からない...。
$ echo -n "$str"|awk -F '\n' -v ORS='__,__' '{print}' hello__,__world__,__
タブ幅の設定
$ 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文字分)がタブに置き換わる。
区切り文字との合わせ技
- シェルのデフォルトの区切り文字は、スペースとタブと改行である。
- 区切り文字の設定は、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次元の配列としてテキストのデータベースを扱うような処理が可能になるのだ!
- AppleScriptで書いたら何十行にもわたる処理が、たった1行で表現できてしまうシンプルさは、素晴らしいと感じた。(コード全体の見通しが、素晴らしく良くなるのだ)