シェルスクリプトでJavaScript文字列リテラル用のエスケープ

JavaScript文字列リテラル用のエスケープ」という表現は微妙だけど、要はサーバサイドでJavaScriptを動的生成する時に、文字列リテラルとして任意のパラメータを出力したい場合のエスケープ処理。

先日(というか先月)書いたHTMLエスケープ用のシェルスクリプトは、通常のHTMLエスケープ用だ。HTMLの文法にて特別扱いされる最小限の文字をエスケープするので、HTML要素の内容(Content)ないし属性値として使用する文字列に対して適用するのが正しい。

eel3.hatenablog.com

サーバサイドではJavaScriptを動的生成することも時々ある。大抵はJavaScriptの文字列リテラルとして任意のパラメータを出力するスタイルだ。このケースでのエスケープは一般的なHTMLエスケープとは異なる。

  1. 出力したJavaScriptの文字列リテラルJavaScriptコード中で以下のプロパティ/メソッドに適用していて、かつHTML要素として解釈されたくない場合は、HTMLエスケープする。
    • HTMLDocument.write()
    • HTMLDocument.writeln()
    • HTMLElement.innerHTML
  2. JavaScriptの文字列リテラルとして許容される内容にエスケープする。
    • 「\」を「\\」に置換する。
    • 「'」を「\'」に置換する。
    • 「"」を「\"」に置換する。
    • 制御文字をエスケープシーケンスに置換する(例えば改行を「\n」に置換する等)
  3. 追加の処理。
    • JavaScriptのコードの出力先が何らかのタグの属性値の部分であるなら、属性値全体をHTMLエスケープする。
    • JavaScriptのコードの出力先がscriptタグの中であるなら、「</script>」を含まないようにする。
    • JavaScriptのコードの出力先がCDATAの中であるなら、「]]>」を含まないようにする。

「</script>」や「]]>」を含まないようにするにはどうするか? JavaScriptの文字列リテラルの範囲内で「</script>」や「]]>」と等価な別の表現にすればよい。例えばUnicodeエスケープシーケンスに置き換えるのだ。JavaScriptの文字列リテラルでは、 "]]>" と "\u005D\u005D\u003E" は等価だ。

変換元テキストがUTF-8であるという前提で、試しにシェルスクリプトエスケープ処理を実装してみた。escape-jsstrという名前だ。

#!/bin/sh

PATH=/bin:/usr/bin:/usr/local/bin

readonly HEX2='[0-9A-F][0-9A-F]'
readonly SPC='[[:blank:]][[:blank:]]*'

exec iconv -f UTF-8 -t UTF-16BE ${@+"$@"} |
    od -A n -t x1 -v |
    tr '[a-z]' '[A-Z]' |
    sed "s/\\($SPC$HEX2\\)$SPC\\($HEX2\\)/\\1\\2/g
         s/$SPC/\\\\u/g" |
    tr -d '\n'

面倒なので全てUnicodeエスケープシーケンスに変換している。

hexdump(1)を使用できる環境なら、もう少し簡単に実現できる。

#!/bin/sh

PATH=/bin:/usr/bin:/usr/local/bin

exec iconv -f UTF-8 -t UTF-16BE ${@+"$@"} |
    hexdump -v -e '"\\" "u" 2/1 "%02X"'

とはいえ、いつもhexdump(1)を使えるとは限らない。LinuxmacOSFreeBSDなどを使う分には困らないが、Cygwinでは標準では含まれない(別途util-linuxを入れる必要がある)。商用UNIXは触ったことがないが、やはり怪しいようだ。

echo(1)ライクに引数の文字列をエスケープするシェルスクリプトはこんな感じ。

#!/bin/sh

exec echo -n ${@+"$@"} | escape-jsstr

HTMLエスケープネタと同様にとりあえず作ってみたけど、需要あるのかなあ?

終わりに

最終的には、こんな感じになった。