初歩のシェルスクリプトで遊ぶ[sedのモヤモヤを整理したい(1)]
「極める気にはなれないが、ちょっとは便利に使いたい」
sedは私はそんな感じなのだけど。思いついたことがあって、それをメモがてらに書いていきます。
遊びで独学してる素人の勉強メモなんで、調べものには使わんといてください。資料を当たって間違い探しをして勉強する、のならOKです。
記事の趣旨は、「経験でなんとなく覚えたsedだが、よくわからないモヤモヤがあるから、整理したい」です。情報の正確さや実用性は度外視してます。
sed『そのもの1個』は『while read ループ1個』に相当するのではないか
見出し通りで、こんなこと思いついたのが切っ掛けです。簡単なsedスクリプトなら、簡単なシェルスクリプトで真似ができるかもしれない。
最小限のsedモデル
#!/bin/bash # ============================================================================= # ■ 簡易sed v0 # ・標準入力を受け取り、そのまま出力する # ・pコマンドのみ使える # # ============================================================================= # p:パターンスペース(PS)の内容を出力する p_cmd(){ printf '%s\n' "${PATTERN_SPACE}" } NR='0' # 行番号 while IFS= read -r PATTERN_SPACE # 入力を1行ずつシェル変数「パターンスペース」に読み込む do NR=$(( ${NR} + 1 )) # 行番号カウント # sedのコマンドが入る場所---- # ----sedのコマンドが入る場所 echo "${PATTERN_SPACE}" # パターンスペースを出力 done
見ての通りで、普通の「while read」ループで、echoをしてるだけです。入力されたテキストを、そのまま出力します。
readで行の内容を格納するシェル変数が「PATTERN_SPACE」で、これをsedでいう「パターンスペース」に模擬してます。
いきなりなんですが、簡易モデルとしても簡易すぎです。これだとコマンドのいくつかが表現できないので。
「パターンスペース内容を表示するpコマンド」
この中に「p」コマンドを置くと、どのように動作するか。(以降はシェル関数の記述は省略します)
#!/bin/bash NR='0' while IFS= read -r PATTERN_SPACE do NR=$(( ${NR} + 1 )) # sedのコマンドが入る場所---- p_cmd # ----sedのコマンドが入る場所 echo "${PATTERN_SPACE}" done
pコマンドは「echo "パターンスペース"」です。1ループにechoがひとつ追加されたので、すべての行を2行ずつ出力するsedスクリプトになります。
sed -e 'p'
sedで書くと、こう。
$ seq 1 3 | ./sedSimple_0.sh 1 1 2 2 3 3 $ seq 1 3 | sed -e 'p' 1 1 2 2 3 3
このループの中では、「pコマンド」は、行の数だけ、実行してます。3行あったら3回実行をしてる。これを踏まえて……
「アドレス」とは何だろうか
$ seq 1 3 | sed -e '2p' 1 2 2 3
アドレス指定をします。「2」を指定したから、2行目のみpコマンドが実行されて、2行目だけ増えてる。
このsedスクリプトを模擬するなら、どのように書くか。
#!/bin/bash NR='0' while IFS= read -r PATTERN_SPACE do NR=$(( ${NR} + 1 )) # sedのコマンドが入る場所---- if test ${NR} -eq 2 ; then p_cmd fi # ----sedのコマンドが入る場所 echo "${PATTERN_SPACE}" done
if文を使って、「もし2行目ならば、~~を実行する」と表現しました。
「2行目を指定してpコマンドを実行」なのだけど、決して「1行目と3行目の処理中は、消えて無くなってる、わけではない」。毎行の処理で、毎回「if test~」は実行しています。
sedのスクリプトも、これと同じように理解すべきなのではないか。
「2行目を指定してコマンドを適用する」と理解すると、まるでテキストファイルをGUIのエディタで編集するときのように、目視で2行目を選んでいる、ように考えてしまう。そうじゃなくて、毎行毎行「もし、現在の行番号が『2』ならば、~~を実行する」という条件分岐を繰り返している。
正規表現指定も、行番号指定も、どちらも条件分岐だと理解する
正規表現でのアドレス指定と、「y」や「s」の文字入れ替えコマンドが絡むと、「毎行のループで必ず条件分岐をしている」という視点が大事になります。
$ echo 'AAA BBB CCC' | sed -e '/A/y/A/B/' -e '/B/y/B/あ/' あああ あああ CCC
「Aを含む行については、『A』を『B』に入れ替えよ」「『B』を含む行については、『B』を『あ』に入れ替えろ」というsedスクリプトです。
もし「『入力に』Bを含む行」を指定したのなら、1行目まで「あ」になるのはおかしい。これは「ひとつ目のコマンドでAがBに入れ替わって、ふたつ目のコマンドでBを含む行を指定したのだ」って説明されます。が。これは「行を指定した」「アドレスを指定した」と考えるよりも、です。
「パターンスペースの内容を正規表現で検索して、マッチしたかどうか、の条件分岐」だと解釈します。
シェルスクリプトで書き換えます。
#!/bin/bash y_cmd(){ # 改行は「\n」の代わりに「\o000」を使うこと XX=`printf "%s" "${PATTERN_SPACE}" | od -tx1 -An -v | sed -e "s/0a/00/g"` eval 'PATTERN_SPACE=`printf "%s\n" "${XX}" | xxd -r -p | sed -e "'"${1}"'" | sed -e "s/\o000/\n/g"`_' PATTERN_SPACE="${PATTERN_SPACE%_}" } NR='0' while IFS= read -r PATTERN_SPACE do NR=$(( ${NR} + 1 )) if echo "${PATTERN_SPACE}" | grep 'A' 1>/dev/null ; then y_cmd 'y/A/B/' fi if echo "${PATTERN_SPACE}" | grep 'B' 1>/dev/null ; then y_cmd 'y/B/あ/' fi echo "${PATTERN_SPACE}" done
yコマンドについては、sedをそのまま埋め込んでます。いろいろ書いてるのは、改行文字の代わりにヌル文字で処理するためのものです。
「if echo "${PATTERN_SPACE}" | grep 'A'」で、「もしパターンスペースの中に『A』が入ってるならば」の条件分岐になってます。これがsedの正規表現でのアドレス指定にあたってる。
さらに先ほどのsedスクリプトも、少し書き換えます。
$ echo 'AAA BBB CCC' | sed -e '/A/ { > y/A/B/ > } > /B/ { > y/B/あ/ > }' あああ あああ CCC
# インデントして見やすく '/A/ { # もし「A」を含むならば y/A/B/ # 「A」を「B」に入れ替えよ } /B/ { # もし「B」を含むならば y/B/あ/ # 「B」を「あ」に入れ替えよ }'
このように書き換えてみると、sedの数字による行番号指定も、正規表現でのアドレス指定も、同じように「if文で何を条件にしているか」の違いである、と言い換えることができます。
- sedひとつが、while readのループひとつのようなもの。
- sedのコマンドは、「sed内部のループに置くコマンドを、sedというプログラミング言語で実行する順番に」書いている。
- たとえばsedをsコマンド一つで動作させたときは、「sedがsコマンドオプションで動作している」のではない。「ループの中で、テキスト1行にループ1回ずつ、sコマンドを実行している」ということ。
- 行番号指定も、正規表現指定も、同じような条件分岐だ。ループ毎に必ず条件判定をしている。
と思っただけで、ソース読んだわけでも、読めるだけのスキルも無いんで、ただの思い付き書いてるだけですが。
「while read」ループの中のsed
while IFS='' read -r LINE do echo "${LINE}" | sed -e 's/入れ替えたい文字列/入れ替える文字列/g' done
よくある「良くないsedの使い方」なのだけど……。別にいいんじゃないのかな……。
こういうスクリプトでも動作するし、個人的には「while read」に統一して一貫性を持たせるのも良いと思うんで、欠点としていうべきで悪いわけじゃ……と、個人的には思うんすよ。仕様を満たしてて、みんなが読みやすいんなら、それでいいような気はするので。中に条件分岐を追加するのも簡単だし。
でも、動作速度の点では劣るのは、それはしょうがない。
sed -e 's/入れ替えたい文字列/入れ替える文字列/g'
「sedはそのものがwhile readと似ている」と意識しているなら、外側のwhile readそのものが要らない、ということが分かりやすくはなる、と思う。
「sコマンドそのもの」は複数行のテキストも扱える
テキストデータの改行を削除しようとして、
sed -e 's/\n//g'
と書いちゃうのがよくある、って聞きました。
なぜ期待通りに動かないのか。説明は「sedは1行ずつの繰り返し動作をしているから……」とされるようだけれども。この説明は、少し回りくどい。
「sedが内部のsコマンドに文字を渡すとき、既に改行ごとに切り分けてしまっていて、sコマンドには改行が渡されていないから」「1行の改行のないテキストからは、改行は削れない」と言い換えられます。
つまり、なんとかして改行も丸ごとsコマンドに渡せば、改行を削れる。
$ echo '1111 2222 3333 4444 5555' | sed 's/\n//g' 1111 2222 3333 4444 5555 $ echo '1111 2222 3333 4444 5555' | sed --posix -n '1h;1!H;$x;$s/\n//g;$p' 11112222333344445555
「s/\n//g」も、ちゃんと効いてます。