メタ文字とエスケープシーケンス
色々な言語でのメタ文字とエスケープシーケンスについて紹介します。
正しいメタ文字知識がなければ、文字列中の記号が違う意味に解釈されてしまい、不具合が発生します。
1.メタ文字とは
文字列中(ソースコード中)の「特殊な意味」に解釈される記号や文字のことです。
2.エスケープシーケンスとは
特殊な意味に解釈されないように、記号そのものを表す特殊記号、でしょうか。
制御記号列そのものをエスケープシーケンスと呼ぶ場合もあります。
3. C/C++言語
C/C++言語系では「文字列」「文字」においてメタ文字が使われています。
\\ 円記号
\' 一重引用符
\" 二重引用符
\a ベル(ビープ音)
\b バックスペース
\t タブ
\f 改ページ
\n 改行(ラインフィード)
\r 改行(キャリッジリターン)
\0 NULL文字
\000 ~ \777 8進数文字定数(0x00~0x1FF)
\x00 ~ \xFF ? 16進数文字定数(SJIS/Unicode/Latin1??)
\u0000 ~ \uFFFF Unicode定数(UTF-16)
\U0000 ~ \U10FFFF Unicode定数(UTF-32)
画面制御のエスケープシーケンスについては、下記などを参照
https://qiita.com/sudo00/items/2b2eec07d3099b5ad664
#include <stdio.h>
int main(void){
puts("Hello World!\nWelcome C language\n");
puts("ASCII 0x31 \x31\n");
puts("ASCII 0x32 \x32\n");
return 0;
}
こんな感じでしょうか。
もしこのようなJavaScriptを含むC言語系のソースを出力するプログラムを作る場合、注意すべき点としては「直後が数字等(0-9A-Fa-f)であると、それがエスケープシーケンスに取り込まれることがある」ということです。
#include <stdio.h>
int main(void){
char text[10];
puts("It's fantasy.");
// text = 「"A's"」
text[0] = '"';
text[1] = 'A';
text[2] = '\'';
text[3] = 's';
text[4] = '"';
text[5] = '\n';
text[6] = '\0';
puts(text);
return 0;
}
text[2]
のように一重引用符'
を文字定数に書く場合には、エスケープが必要です。
一方、文字列の場合にはエスケープが必要だった二重引用符"
はtext[0]
とtext[4]
のようにそのまま書くことが可能です。
逆パターンで、文字列の場合にも一重引用符はそのまま書くことができます(It's fantasyがそうです)。
ただ、どちらの場合もエスケープ表記にしても動作するので、そのように書くこともあります。
3-1.CRLFについて
改行コードをインターネット上のデータやファイルに出力する場合「CRLF」にする場合があります。
Microsoft のC/C++の場合「バイナリーモード」の時は、下記のように\r\n
とする必要があります。
が、普通のstdoutのデフォルトではテキストモードになっているので意識する必要はあまりありません。
#include <stdio.h>
int main(void){
FILE *file;
file = fopen(".\\output.txt", "wb");
if(file == NULL) {
fputs(stderr, "Error:can't open output.txt.\n");
return -1;
}
fputs(file, "Hello World!\r\nWelcome C language\r\n");
fclose(file);
return 0;
}
4. JavaScript (or JScript)
文字列のエスケープシーケンスはC/C++言語とほとんど同じです。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Grammar_and_types#%E6%96%87%E5%AD%97%E5%88%97%E3%83%AA%E3%83%86%E3%83%A9%E3%83%AB
\u{XXXXX} Unicode コードポイントエスケープです
ここは違います。
4-1.テンプレート文字列
これ以外にES2015から「テンプレート文字列」という方式があります。
バッククオートを使った、いわゆるヒアドキュメントのような書式です。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Template_literals
let my_text =
`これは私の日記です。
このように複数行の文字列で書くことができます。
中に${expression}式を書くことができます。
`
console.log(my_text);
テンプレート内部では\x や \u \U{}などのエスケープ表記も使えます。
5.HTML
& & & &
< < > <
> > 1 >
" " " "
' ' ' '
※aposはXHTML 1.0/HTML5から
  &#a0;
※改行しない空白
文字参照(文字実体参照と数値文字参照)といわれる表記です。
細かい話をするとCDATAとかPCDATAという用語が出てきて面倒ですが、今回は飛ばします。
HTML5の文字参照の一覧はこちらにございます。
https://html.spec.whatwg.org/multipage/named-characters.html
思ったよりたくさんあるので、文字参照とURLのパラメーター名が同じだとURLが無効になったりします。
要素の属性の値では'
で囲ってあるときは"
を、"
で囲ってあるときは'
を、直接書けます。
<p><a href="https://example.com/myform.php?>=324">No.324</a></p>
↓正しくは
<p><a href="https://example.com/myform.php?&gt=324">No.324</a></p>
こう書く必要があります。
<p><image src="https://example.com/myimg1.webp" alt="It's fantasy 1"><p>
<p><image src="https://example.com/myimg2.webp" alt='It's fantasy 2'><p>
<p><image src="https://example.com/myimg3.webp" alt='"very" good!'><p>
また滅多に目にすることはありませんが、XMLには任意のブロックをCDATAセクションにすることができます。上のテンプレート文字列みたいな感じです。
<?xml version="1.0" encoding="UTF-8"?>
<html>
<script>
<![CDATA[ここから~~
任意の文字列データ。
終了記号が出るまで<や>もそのまま使える。
~~ここまで
]]>
</script>
<body>
ようこそ私のHPへ。
</body>
</html>
6. Visual Basic (or VBScript)
なんとVisual Basicにはメタ文字がありません。
https://atmarkit.itmedia.co.jp/ait/articles/1709/27/news017.html#vbliteral
それで文字列を+
演算子で連結して表現します。
二重引用符"
は""
でエスケープすることができます。SQLとかコマンドラインと同じですね。
Chr
関数はJavaScriptでいうString.fromCharCode
関数のような機能です。
Console.WriteLine("ハローワールド!" + vbCrLf + "ようこそVBへ")
Console.WriteLine("二重引用符「""」だけはエスケープがあります")
Console.WriteLine("Chr関数を使うこともできます。" + Chr(10) + "どうでしょうか")
7. パーセントエンコーディング
普段見かける処理にURLの「パーセントエンコーディング」というものがあります。
これは現在ではURLの一部分をUTF-8の%xxで表したものです。
しかし、古くは文字コードが決まっておらず、はてなキーワードなどはEUC-jpでした。
http://d.hatena.ne.jp/keyword/%A4%CF%A4%C6%A4%CA%A5%AD%A1%BC%A5%EF%A1%BC%A5%C9
こういうのですね。
https://d.hatena.ne.jp/keyword/%E3%81%AF%E3%81%A6%E3%81%AA%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89
現在はUTF-8へリダイレクトされます。
UTF-8の日本語だとほぼ3文字なので3文字が3倍になって1文字が9文字というとんでもないことになっています。
レガシー継ぎ足しシステムなので、仕方がないとはいえ、もう少しなんとかならなかったのだろうか、とは思います。
Pukiwiki 1.4以前などでも、EUC-jpのURLが見られるようです。
UTF-8でURLが長くなる現象は、もうどうにもなりませんね。主要サイトでもそうです。
例えば、Wikipediaなんかも同じ状況になっています。
『日本語 - Wikipedia』
https://ja.wikipedia.org/wiki/%E6%97%A5%E6%9C%AC%E8%AA%9E
もともとの使用方法は、どちらかというと、検索エンジンのクエリーですね。
『日本語 - Google 検索』
https://www.google.com/search?q=%E6%97%A5%E6%9C%AC%E8%AA%9E
このようにして使います。
8. SQL
太古の間違ったプログラミングでは生のSQL文そのものを文字列と連結して組み立てていました。
public class MySQLQuery1{
String getDayMessage(Connection db, String date){
String ret = "";
try {
String sql_text = "SELECT * FROM BBS_TEXT ";
sql_text += "WHERE BBS_DATE = \"" + date + "\" ";
PreparedStatement ps = db.prepareStatement(sql_text);
Result re = ps.executeQuery();
if(re.next()){
ret = re.getString("honbun");
}
}
return ret;
}
}
dateのバリデーションチェックなどをしていないのであれば、SQLインジェクションし放題ですね……。
public class MySQLQuery2{
String getDayMessage(Connection db, String date){
String ret = "";
try {
String sql_text = "SELECT * FROM BBS_TEXT ";
sql_text += "WHERE BBS_DATE = ? ";
PreparedStatement ps = db.prepareStatement(sql_text);
ps.SetString(1, date); // 1開始
Result re = ps.executeQuery();
if(re.next()){
ret = re.getString("honbun");
}
}
return ret;
}
}
本来はPreparedStatementはこのように?
というプレースホルダーを設定して、値をあとで設定します。
これで文字列が正しく内部でエスケープされたSQLに処理されます。
9.正規表現
正規表現でもエスケープ記号は\
です。
(
から)
までを選択するとなると、()
にエスケープが必要で\(.+\)
というような表記になります。
9-1. 正規表現リテラル
Perl, Rubyなどでは /正規表現/
という形式が使えます。
text = "あいうえおパンケーキだよ"
puts /[ァ-ヴ]+ケーキ/.match(text)
/正規表現/g
とか /正規表現/s
とか後ろにはオプションをつけることもできます。
ただし、ちょっと字句解析の時に、割り算の/
とうまく場合分けできるか、筆者はあまり自信がありません。
またシングルラインコメント//
も言語によってはあります。
C/C++では今もこの表記では導入していません。
しかし、JavaScript、JScript、ECMAScriptでは導入されていますね。
num1 = 1;
num2 = 2;
result = num1 / num2 /m;
regex = / num3 /
comment_q = //g this is comment?
このような空の正規表現がコメントになる問題は、下記の記事に記述があります。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/RegExp/toString
"/(?:)/"
このような表現を用いるということのようです。
演算子の直後にある/
であれば、正規表現ですね。おそらく。
正規表現リテラルを使わないと、文字列で表記する必要があり、エスケープが二重で必要です。
ちょっとややこしいですね。
これはC++でも同様の問題が発生していました。
function text_to_parentheses(str){
let re = new RegExp("\\(.+\\)", "g");
let ret = "";
str.replace(re, function(s){
ret += s + "\n";
});
return ret;
}
9-2. C++の raw string リテラル
そこで、C++ではエスケープを処理しない文字列を導入しました。
#include <string>
std::string s1 = R"(~~)";
std::string s2 = R"なにがし(~~)なにがし";
std::string regex_raw_string(){
std::string re_str = R"xx(\(.+\))xx";
return re_str;
}
こういうのですね。ちょっと例が()だったので分かりにくいですね。
9-3. 置換後文字列
reg_exp = /(abc)(.+)(xyz)/
ret = str.replace(reg_exp, "$$ $2 $1");
正規表現の置換では、置換後文字列で$
がメタ文字です。
$$
$記号
$&
マッチ文字列全体
$1
$2
$3
... 文字列キャプチャ
検索パターンの()の中身
$_
検索文字列全体
詳細はMDCでも。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/replace
文字列指定なので、ついうっかりエスケープ処理を忘れると、こういうレアケースで不具合が出ちゃいますね。
だいだいは文字列で指定するので、同時に\
によるエスケープを受け付けます。
ret = str.replace(/\n/g, "<br>\r\n");
10. Rich Text Format(RTF)、Tex
これらの系譜ではC言語のエスケープに似た「\begin{~~}
」や「~~\par~~
」のような書式を使って、マークアップしていきます。
RTFは以前、Windows95-2000のころのヘルプシステムで使われていました。
またワードパッドの内部形式やファイル形式としても使われていましたが、現在はあまり使われなくなりました。
しかし2022年頃にWindowsの新「メモ帳」の内部処理がText
コンポーネントからRichText
に変更され、軽量のマークアップ言語としてプログラム内部で利用されています。
TexはLaTeXとして、論文の記述などの専門分野でよく使われています。
11. C言語入出力
C言語の入出力関数では、%d
や%f
といったものが登場します。
#include <stdio.h>
int main(void){
int x = 100, y = 75;
int i = y * 100 / x;
printf("結果 %d%%\n", i);
}
実はコンパイラ側が賢くなったため、最近ではこの%d
や%f
の型が一致していないと警告が出るようになりましたが、長い間、間違っていてもそのままコンパイルが出来ていました。
今はずいぶんと良くなりましたね。
これを「printfのフォーマット指定子」などといいます。同様にscanf関数にも似た書式のものがあります。
このフォーマット指定子では%
がメタ文字ですね。
この書式は簡単に数値を整形できるため、いまでも割と人気です。
たまにこのprintf系関数の「書式」部分に、任意のユーザー文字列を直接入力するプログラムがあり、バグが発生することがあります。
某MMORPGのログ出力でも経験しましたし、日本製携帯電話(ガラケー)でも同様の事件があり話題になったことがあります。
ユーザー入力を受け付けるときは%s
などを「指定子」に書いて、後ろの引数でユーザー入力を入れるほうが安全です。
なんならなるべくprintf系関数以外を使う方がいいでしょう。
#include <stdio.h>
void my_function(const char* in_text){
printf("%s", in_text);
printf(in_text); // BUGGY
}
11. 自己記述性
自分の書式を自分の言語で書けるのを「自己記述性」といいますね。
多くの場合、任意の文字列を書けるためには、メタ文字列と同じ普通の文字列を自分自身の言語で書ける必要があります。
C言語系ではそのために「\\
」で「\\
」をエスケープできるため「\n
」も「\\n
」と書くことで回避することができます。
VBでも「""
」が特別扱いなのも、そのためですね。
ところが「俺俺言語」「俺俺マークアップ」では、この自己記述性を満たしていない仕様がちょくちょくあり、完全に任意の文字列を書けないケースがあります。
11.まとめ
言語によって、さまざまなエスケープ表現があることが分かりました。