AWKの文字置換 gsub() 関数の落とし穴

AWKの文字置換 gsub 関数の落とし穴

AWKの文字置換 gsub 関数の落とし穴

HTMLエスケープ処理で遭遇した落とし穴

2023年11月、HTMLエスケープ処理をAWKで書いたときにgsub() 関数の落とし穴にはまって解決に苦労しました。
同じ間違いをしないように、その問題点と解決方法をメモしておきます。
同じ現象で悩んでいる技術者がいるかもしれませんね。このブログが参考になれば幸いです。

HTMLエスケープ処理とは

HTML文書は、山括弧(< >)で囲んだHTMLタグを使用して文書の要素を記述します。
例えば、下線を引くときは次のようにHTMLタグを書きます。


<u>下線</u>

このように、山括弧(< >)でHTMLタグを記述します。
しかし、左山括弧(<)という文字そのものを現す場合は、HTMLエスケープ文字を使用して「&lt;」と記述します。

HTML文書では、いくつかの特殊な文字をエスケープ記号で記述します。

HTMLのエスケープ文字

HTMLのエスケープ文字

HTMLエスケープ処理とは、特殊な文字をエスケープ記号に変換したり、逆に復元する処理です。

素晴らしいツール AWK

HTMLエスケープ処理をどの言語でプログラミングするかを考えたとき、私の使い慣れているAWKが候補にあがりました。

AWKバイブル本

AWKバイブル本

上記に示した技術図書の2冊が私のAWKバイブルです。
今から30年も前の1990年代前半にこれらの本を読んでから、ちょっとしたスクリプトはAWKで書いてきました。

UNIX Power Tools sed & awk プログラミング

プログラミング言語AWK

AWKで書いたエスケープ処理

AWKでエスケープ処理「escape()」と逆変換「unescape()」を書きました。
gsub()関数で文字列置換すれば良いので簡単なものです。

エスケープ文字操作関数

エスケープ文字操作関数

テストするための簡単なスクリプト(cv.sh)も作成しました。
テストデータを入力して、3つのストリングを出力します。

  1. 入力データを出力(原文)
  2. 入力データをエスケープ処理 escape() したストリング
  3. それをさらに逆変換 unescape() したストリング(元の文)

#!/bin/sh
#
cat data.txt |
awk '
function escape(str) {
   gsub(/\&/  ,"\\&amp;"  ,str);   
   gsub(/</    ,"\\&lt;"   ,str); 
   gsub(/>/    ,"\\&gt;"   ,str);   
   gsub(/\"/    ,"\\&quot;" ,str);   
   return(str);
}
function unescape(str) {
   gsub(/&amp;/  ,"&",str); 
   gsub(/&lt;/   ,"<" ,str); 
   gsub(/&gt;/  ,">"  ,str);    
   gsub(/&quot;/ ,"\"" ,str);    
   return(str);
}
{
   data1 = $0;
   data2 = escape(data1);
   data3 = unescape(data2);
   print data1;
   print data2;
   print data3;
}
'

入力データを下記に示します。


$ cat data.txt
R&D : <"R"esearch and "D"evelopment>

さっそく実行してテストします。


$ ./cv.sh
R&D : <"R"esearch and "D"evelopment>
R&amp;D : &lt;&quot;R&quot;esearch and &quot;D&quot;evelopment&gt;
R&amp;D : <"R"esearch and "D"evelopment>
$

ちょっと結果がおかしくなりました。
エスケープ処理は、正常に動作して特殊な文字をエスケープ記号に変換しています。
しかし、逆変換ではアンパサンド記号(&)が変換されませんでした。

AWK文字置換 gsub() 関数の落とし穴

AWKの正規表現では、アンパサンド記号(&)が特別な意味を持ち「/&amp;/」を探せないのかもしれません。
文字置換 gsub() 関数は、置換数を戻り値とするのでデバックプリントを入れてみました。


function unescape(str) {
   n=gsub(/&amp;/  ,"\\&",str); 
   print n,str; 
   gsub(/&lt;/   ,"<" ,str); 
   gsub(/&gt;/  ,">"  ,str);    
   gsub(/&quot;/ ,"\"" ,str);    
   return(str);
}

それでは、実行してみます。


$ ./cv.sh
1   R&amp;D : <"R"esearch and "D"evelopment>
R&D : <"R"esearch and "D"evelopment>
R&amp;D : &lt;&quot;R&quot;esearch and &quot;D&quot;evelopment&gt;
R&amp;D : <"R"esearch and "D"evelopment>
$

文字置換 gsub() 関数の置換数は1ですので、1つの文字置換をしています。
しかし、置換後の文字列には変化がありません。

AWKの文字置換 gsub() 関数において、アンパサンド記号(&)は特別な意味がありそうです。

落とし穴の原因

インターネットで検索したり、AWKバイブル本を読み直して、このような結果になった原因を探りました。

バイブル本に、その答えが書いてありました。

AWK gsub関数の「&」の扱い

AWK gsub関数の「&」の扱い

文字置換 gsub() 関数の置換文字の中のアンパサンド記号(&)は、正規表現で一致した文字列を示すことが判明しました。
AWKを使い慣れていたのですか、この機能については知りませんでした。

解決方法

原因がわかりましたので、置換文字列を下記のようにすればよいわけです。


gsub(/&amp;/  ,"\\&",str); 

関数 unescape() は、次のようになります。


function unescape(str) {
   gsub(/&amp;/  ,"\\&",str); 
   gsub(/&lt;/   ,"<" ,str); 
   gsub(/&gt;/  ,">"  ,str);    
   gsub(/&quot;/ ,"\"" ,str);    
   return(str);
}

実行してみます。


$ ./cv.sh
R&D : <"R"esearch and "D"evelopment>
R&amp;D : &lt;&quot;R&quot;esearch and &quot;D&quot;evelopment&gt;
R&D : <"R"esearch and "D"evelopment>
$

正しい結果となりました。

AWKは素晴らしいツール

定年退職してから趣味のプログラミングをしていますが、AWKの出番が減りました。
久しぶりにAWKを書いてみて、AWKは素晴らしいツールだと感じました。

「プログラミング言語AWK」の第8章 エピローグの結論に以下のような記述があります。

Awkは、あらゆるプログラミングの問題に対する解答というわけではないが、プログラマの道具箱のなかの一つの道具として必要不可欠なものである。

確かに、プログラマの道具箱の中に入れて、使いたいときに活用できるようにしておくべきだと思いました。