初歩のシェルスクリプトで遊ぶ[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行につなぐ

sedのちょっと進んだ使い方 - k-igrsの日記

N_cmd
s_cmd 's/\o000/_/'
$ cal
      102020        
日 月 火 水 木 金 土  
             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 
      102020        _日 月 火 水 木 金 土  
             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/_/'
      102020        _日 月 火 水 木 金 土  
             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 
      102020        
日 月 火 水 木 金 土  
             1  2  3  
25 26 27 28 29 30 31  
                      
$ cal | sed -e '/4/d'
      102020        
日 月 火 水 木 金 土  
             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'
日 月 火 水 木 金 土  
      102020        
 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
日 月 火 水 木 金 土  
      102020        
 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