初歩のシェルスクリプトで遊ぶ[sedのモヤモヤを整理したい(2)]

今回はパターンスペースとホールドスペースを、シェルスクリプト製の簡易sedに実装します。
内容は「整理したい(1)」で書いた内容の再説明になってますが、繰り返して噛み砕きます。

「2p」は「2行目を出力」ではなく「行番号カウンターが2のとき、変数『パターンスペース』内の文字を全て出力」

# 2行目にマッチして出力?
$ seq 1 3 | sed -e '2p'
1
2
2
3
# 2行目にマッチして削除?
$ seq 1 3 | sed -e '2d'
1
3

前回「整理したい(1)」で例にした、2行目を重複出力する例と、定番の2行目を削除するスクリプト
特に「指定した行を削除するには」という目的で使われたり紹介されたりする例ですが、「2」は「2行目を指定して」と説明されることがあるようです。これが誤解の元になる。
「2行目を」というと、「入力されたテキストデータの2行目を」という意味にも聞こえます。場所なんでしょうか内容なんでしょうか。
もし、2行目と1行目の両方が、パターンスペースに入っていたら、どうなるんだろうか。sedがパターンスペースの内容を調べて、2行目の部分だけを相手にするんだろうか。

「2行目のテキストを」と「行番号カウンタが2ならば」は違う

https://linuxjm.osdn.jp/html/GNU_sed/man1/sed.1.html
説明書によると、sedのpコマンドは「パターンスペースの内容を出力する、プリントする」です。「アドレスで指定した行を出力する」のとは、どこかが違う。

$ seq 1 2 | sed -n -e '2p'
2
narr@UV:~$ seq 1 2 | sed -n -e '1h;2H;2x;2p'
1
2
narr@UV:~$ seq 1 2 | sed -n -e '1h;2H;2x;2p;2s/\n/_/g;2p'
1
2
1_2

自動出力を抑制する「-n」で静かにさせておいて、「2p」のみであれば、2行目のみ出力をする。
1行目をホールドスペースにコピーしておき、2行目をホールドスペースに追記して、ホールドスペースとパターンスペースを入れ替え、そして「2p」すると、1行目も出力されます。「1(改行)2」の状態で。「2p」が「2行目をプリントせよ」なら、1行目まで出てくるのはおかしい。
「1(改行)2」を、sコマンドで「改行をアンダースコアに入れ替えよ」と命令すると、「1_2」になる。最後の「2p」は、「2行目をプリント」では全くなくなっている。

たまたま「アドレス2を指定したときに、パターンスペースの中に2行目の内容のみが入っていた」ならば、2行目のみが出力される。もし「アドレス2」を指定したときに、1行目の内容も一緒にまとめてパターンスペースに入っていれば、1行目と2行目の内容が一緒に出力されてしまう。

sedの「行番号でのアドレス指定」を、「入力されたテキストデータの行を指定する」と理解すべきではありません。

  • sedは、内部に行番号を数えるカウンターの数値を持っている。1行読み込むと同時に、+1する。
  • sedの「行番号アドレス指定」とは、「カウンターの数字を見なさい」という意味。

sed内部の行番号カウンタの数値を見て、『もし行番号カウンタが2ならば』という条件分岐をしている」と解釈したほうが、理解しやすくなります。

#!/bin/sh
# =============================================================================
# ■ 簡易sed パターンスペースとホールドスペース
# =============================================================================

# s
s_cmd(){
# sedをそのまま埋め込んである。
# 改行は「\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}"
}

# パターンスペース(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}"
}


# =============================================================================
# 処理開始
# =============================================================================

# sedスクリプト内で直接に使うシェル変数_アドレス指定に使う
NR='0'               # 行番号

# 直接は使わないが操作するシェル変数
PATTERN_SPACE=''     # パターンスペース
HOLD_SPACE=''        # ホールドスペース

# 自動出力の抑制オプション
Parm_n='-n'

# メインループ開始

while IFS= read -r PATTERN_SPACE ; do # ==== 最初の1行読み込み =========
# 行番号を+1
NR=$(( ${NR} + 1 ))
# ---- sedスクリプト記述領域/ ------------------------------------------


if test ${NR} -eq 1 ; then
h_cmd
fi

if test ${NR} -eq 2 ; then
H_cmd
fi

if test ${NR} -eq 2 ; then
x_cmd
fi

if test ${NR} -eq 2 ; then
s_cmd 's/\o000/_/g'
fi

if test ${NR} -eq 2 ; then
p_cmd
fi


# ------------------------------------------ /sedスクリプト記述領域 ----
# ==== 自動出力 「-n」で抑制する =======================================
if test "${Parm_n}" != '-n' ; then
printf '%s\n' "${PATTERN_SPACE}"
fi

done
$ seq 1 2 | ./sedSimple_PS_HS.sh 
1_2

ホールドスペースの変化を見る

パターンスペースとホールドスペースで処理するsedスクリプトは、ややこしいといえばややこしいです。でも「変数の定形操作」だと思えば、そんなに難解な処理でもないように思えます。
新規に変数を作って追加、といったことはできないが、自由に使える変数が一つだけ用意されている。それがホールドスペースだ、くらいに理解するのが、手っ取り早い。

【 sed 】コマンド(応用編その3)――ホールドスペースの活用:Linux基本コマンドTips(214) - @IT

@ITの例を、シェルスクリプトで再現しつつ、さらにホールドスペースの内容の変化を見ます。

シェルスクリプトには「最後の行」の指定を実装してないので、行数を固定して数字で指定することにします。@ITの例だと、こう。

$ echo '1行目
2行目
3行目' | sed -n -e '1!G;h;$p'
3行目
2行目
1行目
$ echo '1行目
2行目
3行目' | sed -n -e '1!G;h;3p'
3行目
2行目
1行目


#!/bin/sh
#set -x
# =============================================================================
# ■ 簡易sed パターンスペースとホールドスペース
# =============================================================================


# p:PSの内容を出力する
p_cmd(){
printf '%s\n' "${PATTERN_SPACE}"
}

# パターンスペース(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}"
}

# ----【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/'
) }

# =============================================================================
# 処理開始
# =============================================================================

# ホールドスペース表示用
holdSpaceTemp=`mktemp`
trap 'rm "${holdSpaceTemp}"' 0 1 3 15


# sedスクリプト内で直接に使うシェル変数_アドレス指定に使う
NR='0'               # 行番号

# 直接は使わないが操作するシェル変数
PATTERN_SPACE=''     # パターンスペース
HOLD_SPACE=''        # ホールドスペース


Parm_n='-n'

# メインループ開始

while IFS= read -r PATTERN_SPACE ; do # ==== 最初の1行読み込み =========
# 行番号を+1
NR=$(( ${NR} + 1 ))
# ---- sedスクリプト記述領域/ ------------------------------------------
# sed -ne '1!G;h;3p'

if test ${NR} -ne 1 ; then
G_cmd
fi


h_cmd


if test ${NR} -eq 3 ; then
p_cmd
fi


# ------------------------------------------ /sedスクリプト記述領域 ----
# ==== 自動出力 「-n」で抑制する =======================================
if test "${Parm_n}" != '-n' ; then
printf '%s\n' "${PATTERN_SPACE}"
fi

HS_PS_viewSub

done