初歩のシェルスクリプトで遊ぶ[sedのモヤモヤを整理したい(3)]
使いやすいコマンドに絞り、注意が必要なコマンドを避ける
sedのコマンドにも、よく使うコマンド、あまり使わないコマンド、があります。よく使うのはsコマンド、あまり使わないのはGNU拡張の「シェルでシステムのコマンドを実行するe」とか。
シェルスクリプトでsedを真似するときでも、思ったより簡単に真似できるものもあれば、意外とややっこしいものもある。
コマンドを大雑把に分けてみます。
- 「while read PATTERN_SPACE ; do~」で書き始めた場合に、比較的手軽に実装できるコマンドは実装する
- 基本の「while read」ループを書き換える必要がある、たとえば「while true ;do~」にしなければならないコマンドは、除外する
たとえば、大文字のDのコマンドがそうなんですが、「2行以上あるならば、1行だけ削除し、新しく行を読み込まずに次のサイクルを開始する」ので、ループの先頭に戻っても、readしちゃいけない。
「continue」で「while read」の先頭に戻ると、どうしてもreadしてしまう。Dコマンドを入れるなら、「while read」は使えない。試作はしたんですが、やっぱりこれだけでも読みにくくなりました。
ラベルとジャンプはどうしたらいいか分からないし、「行の後ろにテキストを追加するaコマンド」も、意外と面倒です。
手間をかけずに書ける範囲と、例外として「よく使うコマンド」は妥協して追加して、書いてみたのが以下です。
簡易sed_書いてみたばかりで未テスト
#!/bin/sh #set -x # ============================================================================= # ■ 簡易sed 「while read」による簡易モデル v0.2 # 必要:xxdコマンド # ============================================================================= # パターンスペース文字を加工する(基本的に出力は行わない)---------------------- # s s_cmd(){ # sedをそのまま埋め込んである。sもyも同じである。 # 改行は「\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%_}" } _s_cmd(){ # xxdコマンドを使わない版_念の為 XX="`printf "%s" "${PATTERN_SPACE}" | od -to1 -An -v | sed -e 's/012/000/g;s/ /\\\\\\\/g'`" eval 'PATTERN_SPACE=`printf "%s\n" "${XX}" | xargs -I "{}" printf "{}" | sed -e "'"${1}"'" | sed -e "s/\o000/\n/g"`_' PATTERN_SPACE="${PATTERN_SPACE%_}" } # y 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%_}" } # パターンスペース文字や与えた文字を出力する----------------------------------- # p:PSの内容を出力する p_cmd(){ printf '%s\n' "${PATTERN_SPACE}" } # P:PSの一行目を出力する P_cmd(){ printf '%s\n' "${PATTERN_SPACE}" | head -n 1 } # i:与えたテキストを直ちに出力する i_cmd(){ printf '%s\n' "${1}" } # =:行番号を出力する equal_cmd(){ printf '%s\n' ${NR} } # l:(小文字のエル) 現在のPSを誤解の無い表現で出力する_odコマンドで代用 l_cmd(){ printf '%s\n' "${PATTERN_SPACE}" | od -tc -An -v } # q:PSの内容があれば出力してから、終了する q_cmd(){ # もし「-n」が無いならば、PSを出力してから if test "${Parm_n}" != '-n' ; then echo "${PATTERN_SPACE}" fi exit 0 } # パターンスペース(PS)とホールドスペース(HS)操作 --------------------------------- # h h_cmd(){ HOLD_SPACE="${PATTERN_SPACE}" } # H H_cmd(){ HOLD_SPACE="${HOLD_SPACE} ${PATTERN_SPACE}" } # g g_cmd(){ PATTERN_SPACE="${HOLD_SPACE}" } # G G_cmd(){ PATTERN_SPACE="${PATTERN_SPACE} ${HOLD_SPACE}" } # x x_cmd(){ buffer="${PATTERN_SPACE}" PATTERN_SPACE="${HOLD_SPACE}" HOLD_SPACE="${buffer}" } # 「簡単実装」の趣旨に反するが例外で実装するコマンド ----------------------------- # d # 「d_cmd && continue」と使うこと d_cmd(){ PATTERN_SPACE='' return 0 } # n:標準入力から1行読み込み n_cmd(){ # もし「-n」が無いならば、PSを出力してから if test "${Parm_n}" != '-n' ; then echo "${PATTERN_SPACE}" fi # もし標準入力にデータが有るならば、 if IFS= read -r Buffer ; then # PSに1行、上書きで読み込む PATTERN_SPACE="${Buffer}" # 行番号を+1 NR=$(( ${NR} + 1 )) else # readできなかったならば、 # (現在PSに内容が入っていても)即座に終了する exit 0 fi } # N:標準入力から1行読み込んで追記 N_cmd(){ # もし標準入力にデータが有るならば、 if IFS= read -r Buffer ; then # 1行を読み込み、PSに改行追記する PATTERN_SPACE="${PATTERN_SPACE} ${Buffer}" # 行番号を+1 NR=$(( ${NR} + 1 )) else # 標準入力が空っぽならば、 # (現在PSに内容が入っていても)即座に終了する exit 0 fi } # ----【debug】--------------------------------------------------------- HS_PS_viewSub(){ ( printf '%s\n' "${HOLD_SPACE}" 1> "${holdSpaceTemp}" echo "${PATTERN_SPACE}" | paste -d "`printf '\034'`" - "${holdSpaceTemp}" | xargs -I '{}' echo "${NR}:"'{}' | column -t -s "`printf '\034'`" | sed -e 's/.*/\o033[30;102m&\o033[0m/' ) } # ============================================================================= # 処理開始 # ============================================================================= # sedスクリプト内で直接に使うシェル変数_アドレス指定に使う NR='0' # 行番号 DOLLAR='' # 最終行の行番号 # 直接は使わないが操作するシェル変数 PATTERN_SPACE='' # パターンスペース HOLD_SPACE='' # ホールドスペース # 最終行数を取得するため、いったん標準入力をファイルへ stdinData=`mktemp` holdSpaceTemp=`mktemp` cat - 1> "${stdinData}" #trap 'rm -v "${stdinData}" "${holdSpaceTemp}"' 0 1 3 15 trap 'rm "${stdinData}" "${holdSpaceTemp}"' 0 1 3 15 DOLLAR=`cat "${stdinData}" | wc -l` Parm_n="${1}" #Parm_n='-n' # メインループ開始 cat "${stdinData}" | while IFS= read -r PATTERN_SPACE ; do # ==== 最初の1行読み込み ========= # 行番号を+1 NR=$(( ${NR} + 1 )) # ---- sedスクリプト記述領域/ ------------------------------------------ # ------------------------------------------ /sedスクリプト記述領域 ---- # ==== 自動出力 「-n」で抑制する ======================================= if test "${Parm_n}" != '-n' ; then printf '%s\n' "${PATTERN_SPACE}" fi #HS_PS_viewSub # 【debug】 done
小文字のdコマンドは、シェル関数のあとに続けて「 && continue」が必要です。
他のコマンドは、たとえば処理を中断してサイクルの先頭に戻る、この場合は「continue」だとかは、ありません。終了する「exit」があるくらい。
sedを「同じ書き方の繰り返し」に落とし込む
sed -e '2d' sed -e 's/ABC/あいうえお/' sed -e '5y/ABC/abc/'
sedは言葉に見えません。単語の区切りもわかりません。分かる人が簡単に書くには良いんでしょうが、分からん人にとっては、とっかかりが悪い。
これが、単語の切れ目が分かって、書き方のパターンが一定なら、だいぶ違います。
sed -e ' 2{ d } sed -e ' { s/ABC/あいうえお/ } ' sed -e 5{ y/ABC/abc/ }
ぜんぶを括弧で括ります。
アドレス指定 {
コマンド
}
こうすると、「もし~ならば、{ コマンド }を実行せよ」の見た目に統一される。スクリプトっぽくなります。
「2d」は、「2」は「行数カウンタが2ならば括弧内コマンドを実行せよ」であって、「dコマンドに2を渡した」のではない。それが見てわかります。
シェルスクリプトならば「if [ 行番号カウンタ -eq 数字] then ~」「if echo ~~ | grep ; then ~~」と似たようなものだと考えれば、sedも書きやすくなる。もちろん別の慣れた言語で置き換えてもいい。
スクリプト例
シェルスクリプトのsedもどき用のスクリプトをいくつか。
「sedスクリプト記述領域」に下記のを追記すると、本物のsedに似せて動きます。
2行を1行につなぐ
N_cmd s_cmd 's/\o000/_/'
$ cal 10月 2020 日 月 火 水 木 金 土 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 $ cal | ./sedWhileRead_0_1.sh 10月 2020 _日 月 火 水 木 金 土 1 2 3 _ 4 5 6 7 8 9 10 11 12 13 14 15 16 17 _18 19 20 21 22 23 24 25 26 27 28 29 30 31 _ $ cal | sed -e 'N;s/\n/_/' 10月 2020 _日 月 火 水 木 金 土 1 2 3 _ 4 5 6 7 8 9 10 11 12 13 14 15 16 17 _18 19 20 21 22 23 24 25 26 27 28 29 30 31 _
「4」を含む行を削除する
if echo "${PATTERN_SPACE}" | grep '4' 1>/dev/null ; then d_cmd && continue fi
$ cal | ./sedWhileRead_0_1.sh 10月 2020 日 月 火 水 木 金 土 1 2 3 25 26 27 28 29 30 31 $ cal | sed -e '/4/d' 10月 2020 日 月 火 水 木 金 土 1 2 3 25 26 27 28 29 30 31
シェルスクリプトはcontinueしてるだけで、削除に意味が無いんですが。
実際のsedだと「パターンスペースを削除」が重要な意味を持ってるっぽい気がする。「削除」は、シェル変数を空にするのとは違ってて、「パターンスペースそのものが無くて続きの処理ができません、だから最初に戻ってパターンスペースを作り直します」というような。
偶数行と奇数行を入れ替える
h_cmd n_cmd p_cmd g_cmd p_cmd
$ cal | sed -n -e 'h;n;p;g;p' 日 月 火 水 木 金 土 10月 2020 4 5 6 7 8 9 10 1 2 3 18 19 20 21 22 23 24 11 12 13 14 15 16 17 25 26 27 28 29 30 31 $ cal | ./sedWhileRead_0_1.sh -n 日 月 火 水 木 金 土 10月 2020 4 5 6 7 8 9 10 1 2 3 18 19 20 21 22 23 24 11 12 13 14 15 16 17 25 26 27 28 29 30 31
改行区切りのテキストを「_」で繋ぐ
if test ${NR} -eq 1 ; then h_cmd fi if test ${NR} -ne 1 ; then H_cmd fi if test ${NR} -eq ${DOLLAR} ; then x_cmd fi if test ${NR} -eq ${DOLLAR} ; then s_cmd 's/\o000/_/g' fi if test ${NR} -eq ${DOLLAR} ; then p_cmd fi
$ seq 0 9 | sed --posix -n '1h;1!H;$x;$s/\n/_/g;$p' 0_1_2_3_4_5_6_7_8_9 $ seq 0 9 | ./sedWhileRead_0_1.sh -n 0_1_2_3_4_5_6_7_8_9