Lua スクリプト「a={十=10}
」
AviUtl「止まれ!!!」
*** ど う し て ***
登場人物紹介
-
AviUtl
動画編集フリーソフト最大手。Windows 専用。
筆者環境では本体が ver 1.00、拡張編集が ver 0.92。拡張編集機能では、Lua 5.1 によるスクリプト制御でかなり色々な処理を自力実装できるので便利。
ただし、ソフト内で扱えるテキストやスクリプトファイルの文字エンコーディングは Shift_JIS のみ。 -
Lua
今日も AviUtl スクリプター達にこき使われている。
上記の通り、AviUtl に搭載されている Lua は ver 5.1。
世にある最新の Lua は ver 5.41 なので、お間違い無きよう。
Lua のテーブル記法
Lua のテーブルは、キーと値の組み合わせを色々な書き方で代入できる。
---- この書き方は、
tbl1 = {}
tbl1["key"] = "value"
---- この書き方と同じ。
tbl2 = {}
tbl2.key = "value"
---- そしてこの書き方とも同じ。
tbl3 = {["key"] = "value"}
---- 更にこの書き方とも同じ。
tbl4 = {key = "value"} -- 今回問題なのはこれ
上記の tbl2
や tbl4
の例では、キー自体が文字列であるにもかかわらずそれを "
でくくる必要が無い。
というかむしろ、これらの記法ではくくらずに必ず生で書かなければいけない。
これを何と呼ぶのか知らないけど、とりあえず本記事では「生キー」とでも呼ぼうか。
そして、少なくとも AviUtl の Lua スクリプトにおいては2、生キーに使える文字は別に a とか Z とかの ASCII 文字に限られるわけではない。
つまり、tbl.あ
みたいな書き方をしても全然支障無く動く。そっちの方がおかしいだろ
しかし、この「全角生キー」には恐るべき罠が…!
全角生キー、通ったり通らなかったり
AviUtl の拡張編集タイムライン上で適当に「図形」オブジェクトでも置いて、それに「スクリプト制御」エフェクトを追加して、その中に色々記述してみよう。
なお、デバッグ情報出力ソフトとして私は DebugView を使っている。
→ DebugView - Windows Sysinternals | Microsoft Docs
まずは通る例から
a = {八 = 8}
debug_print(a["八"])
8
特に問題無くメッセージが出力される。
いざ、通らない例を
a = {十 = 10}
debug_print(a["十"])
[string "a = {十\ = 10}..."]:1: '}' expected near '\'
は? \
なんて使ってないが?
エラーにあるソースの一部にもなんか \
が入ってるし…。
通らない例2
a = {怖 = 666}
debug_print(a["怖"])
[string "a = {怖 = 666}..."]:1: '}' expected near '|'
だから**どこに |
があんのよ。**怖……。
今度はエラーのソースの中にも |
が無い始末。
通らない例3
a = {マ = true}
debug_print(a["マ"])
[string "a = {マ = true}..."]:1: unexpected symbol near '='
マ?
なぜなのか
A. ダメ文字だから
ダメ文字とは
Shift_JIS の2バイト文字のうち、下位バイトが特殊な ASCII 記号文字と一致してしまって不具合を起こすものを、概してダメ文字と呼ぶ。
…らしい。
正直、この記事を書くために調べるまで知らない概念だった。
もう少し詳しく
1バイト文字について
Shift_JIS において、ASCII 文字と半角カタカナは1バイト文字。
文字コードに1バイトのみを用いるので、その値の範囲は $\textrm{0x00} \sim \textrm{0xFF}$3 となる。
実際には ASCII 文字が $\textrm{0x00} \sim \textrm{0x7F}$ を、半角カタカナが $\textrm{0xA1} \sim \textrm{0xDF}$ を使っていて、他は空いている。
2バイト文字について
で、Shift_JIS のそれ以外の全角文字はもれなく2バイト文字。
例えば「あ」は $\textrm{0x82}$ $\textrm{0xA0}$ で表現される。
(本当はひとまとめに $\textrm{0x82A0}$ と書くべきだろうが、今回は説明の都合上こう表記する)
全角文字の上位バイト、「あ」でいうところの $\textrm{0x82}$ の方は、1バイト文字で使われていない $\textrm{0x81} \sim \textrm{0x9F}$ と $\textrm{0xE0} \sim \textrm{0xEF}$ の範囲を上手く拝借している。
一方の下位バイト、「あ」の $\textrm{0xA0}$ の方は、1バイト文字の実用領域と一部重複するような $\textrm{0x40} \sim \textrm{0xFC}$(ただし $\textrm{0x7F}$ 以外)の範囲をふんだんに利用している。漢字いっぱいあるからね…。
そして悲劇は起こる
AviUtl における Lua スクリプトでは、生キーに使われている文字の下位バイトが ASCII 文字でいう @
[
\
]
^
`
{
|
}
~
の記号のいずれかにあたる値と一致してしまうと、コンピューターがスクリプトのその位置にその記号を見出して読んでしまうらしい。
それゆえ、**「こんなとこに |
を置かれても困るんですけど!!」とか「閉じる }
だけあって開く {
が無いやんけ!!」**みたいな感じで、本来置いていないはずの記号に対してそれ由来の不具合が起こる。
a = {十 = 10}
の例を考えると、
- 2バイト文字である「十」の文字コードは $\textrm{0x8F}$ $\textrm{0x5C}$
- この上位バイトは、1バイト文字のうち未使用領域のなんかと一致する(説明では仮に
★
とする) - この下位バイトは、1バイト文字である半角バックスラッシュ
\
($\textrm{0x5C}$)と一致する - コンピューターは「十」の部分を
★\
という記述だと思ってしまう - コンピューターは行全体を
a = {★\ = 10}
という記述だと思ってしまう - エラー!
みたいな流れで事が起きてしまっていると思われる。
以下、先ほどの通る例・通らない例の文字コード検証スクリプト。
chrBytesHex = function(chr)
local hex = function(byte)
return byte and string.format("0x%0X", byte) or "0x__"
end
return hex(string.byte(chr, 1)) .. ", " .. hex(string.byte(chr, 2))
end
debug_print(chrBytesHex("A")) -- 0x41, 0x__
debug_print(chrBytesHex("ァ")) -- 0xA7, 0x__
debug_print(chrBytesHex("あ")) -- 0x82, 0xA0
debug_print(chrBytesHex("ェ")) -- 0xAA, 0x__ -- 半角カナの「ぇ」
debug_print(chrBytesHex("八")) -- 0x94, 0xAA -- 下位が "ェ"(記号じゃない)と同じ:OK
debug_print(chrBytesHex("\\")) -- 0x5C, 0x__
debug_print(chrBytesHex("十")) -- 0x8F, 0x5C -- 下位が "\"(記号)と同じ:NG
debug_print(chrBytesHex("|")) -- 0x7C, 0x__
debug_print(chrBytesHex("怖")) -- 0x95, 0x7C -- 下位が "|"(記号)と同じ:NG
debug_print(chrBytesHex("}")) -- 0x7D, 0x__
debug_print(chrBytesHex("マ")) -- 0x83, 0x7D -- 下位が "}"(記号)と同じ:NG
ちなみに
さっきの文字コード表の画像は AviUtl で作ったよ!
表組み部分のスクリプト制御はこちら!
local size,color,NG,ctrl={c=22,t=12,g=2},{b=0xFFFFFF,c=0xD0F090,t=0,NG=0xFFFF00,NA=0xDDDDDD},{64,91,92,93,94,96,123,124,125,126},{[0]="NUL","SOH","STX","ETX","EOT","ENQ","ACK","BEL","BS","HT","LF","VT","FF","CR","SO","SI","DLE","DC1","DC2","DC3","DC4","NAK","SYN","ETB","CAN","EM","SUB","ESC","FS","GS","RS","US","SP",[127]="DEL"}
obj.load("figure","四角形",color.b,size.c*17+size.g*18);obj.draw()
for ny=-1,15 do for nx=-1,15 do
local isNG,n,p=false,ny*16+nx,size.c+size.g
local x,y,t,tc,tf,cc,zm=(nx-7)*p,(ny-7)*p,"",color.t,"Migu 1C",color.c,4
if ny==-1 or nx==-1 then
tf,t="Century,B",ny==nx and "" or ("%X"):format(math.max(ny,nx))
else
if n>127 and n<161 or n>223 then t,cc="",color.NA
else
for k,v in pairs(NG) do isNG=isNG or n==v end
t,cc=ctrl[n] or string.char(n),isNG and color.NG or cc
end
tf=ctrl[n] and "Migu 1M" or tf
obj.load("figure","四角形",cc,size.c);obj.draw(x,y)
end
obj.load("text",("<#%06x><s%d,%s>%s"):format(tc,size.t*zm,tf,t))
obj.draw(x,y,0,1/zm)
end end
1024文字という上限に収めるためにギチギチに圧縮してあるので、読みやすさが最悪になっているよ!
良い子のみんなは、大人しくカスタムオブジェクトファイル *.obj
に分離しようね!
なお、下位バイトが記号にあたるなら必ず問題になるというわけではなく、さっきの表を見ての通り _
だけはダメ文字挙動を起こさない。変数名に使える記号だからかも。
また、.
や =
など文字コードが若い記号は、Shift_JIS 全角文字の下位バイト領域から外れているので、全角文字の下位バイトとはそもそも一致し得ない。
じゃあ、具体的に使えない全角文字はどれとどれなの?
「どれ」とかいうレベルじゃなくいっぱいある。
カタカナのうちダメなのは 10 文字
とりあえずカタカナの範囲に絞ると、次のようなスクリプト制御でダメ文字とエラー文が調査できる。
-- どの行のコメントアウトを外しても必ずエラーになるぞ!
-- a = {ァ = 0} -- 0x83 0x40 (@)
a = {ア = 0} -- 0x83 0x41 (A)
a = {セ = 0} -- 0x83 0x5A (Z)
-- a = {ゼ = 0} -- 0x83 0x5B ([)
-- a = {ソ = 0} -- 0x83 0x5C (\)
-- a = {ゾ = 0} -- 0x83 0x5D (])
-- a = {タ = 0} -- 0x83 0x5E (^)
a = {ダ = 0} -- 0x83 0x5F (_)
-- a = {チ = 0} -- 0x83 0x60 (`)
a = {ヂ = 0} -- 0x83 0x61 (a)
a = {ホ = 0} -- 0x83 0x7A (z)
-- a = {ボ = 0} -- 0x83 0x7B ({)
-- a = {ポ = 0} -- 0x83 0x7C (|)
-- a = {マ = 0} -- 0x83 0x7D (})
-- a = {ミ = 0} -- 0x83 0x7E (~)
a = {ム = 0} -- 0x83 0x80 (以下ASCII外なので全部OK)
a = {メ = 0} -- 0x83 0x81
a = {ぁ = 0} -- 0x82 0x9F
a = {あ = 0} -- 0x82 0xA0
a = {◯ = 0} -- 0x81 0xFC (終わり)
obj.zoom = 1/2 -- 通れば縮む
debug_print("OK")
ダメ文字 (カタカナ) |
文字コード | 下位バイト 記号 |
エラー文 |
---|---|---|---|
ァ | $\small \textrm{0x83 0x40}$ | @ | '}' expected near '@' |
ゼ | $\small \textrm{0x83 0x5B}$ | [ | unexpected symbol near '=' |
ソ | $\small \textrm{0x83 0x5C}$ | \ | '}' expected near '' |
ゾ | $\small \textrm{0x83 0x5D}$ | ] | '}' expected near ']' |
タ | $\small \textrm{0x83 0x5E}$ | ^ | unexpected symbol near '=' |
チ | $\small \textrm{0x83 0x60}$ | ` | '}' expected near '`' |
ボ | $\small \textrm{0x83 0x7B}$ | { | unexpected symbol near '=' |
ポ | $\small \textrm{0x83 0x7C}$ | | | '}' expected near '|' |
マ | $\small \textrm{0x83 0x7D}$ | } | unexpected symbol near '=' |
ミ | $\small \textrm{0x83 0x7E}$ | ~ | '}' expected near '~' |
該当する記号によって、エラーの内容も変わることがわかる。
ダメ文字の概念を知らない人がただ "unexpected symbol ~" と怒られた場合、そのエラーだけから問題を逆算するのはかなり大変だろう。(大変でした)
なお、この問題は要するにアカン文字の混入誤認識による文法エラーなので、実用上の場面でどういうエラーが出るのかはその文字の位置や前後の記述によっても当然変わる。
例えば、4行目を a = {ボア = 0}
とするだけで '}' expected (to close '{' at line 4) near 'a'
というまた違ったエラーが出て楽しい。楽しくねえよ
全てのダメ文字(外部サイト参照)
こちらに、ダメ文字になり得る文字がよくまとまっている。
→ ダメ文字一覧表 - fudist
あくまでなり得る――様々な環境を最大公約数的に考慮すればその恐れだけはある――文字であり、実状としてほんとにダメになるかはもちろん個々の環境による。
AviUtl + Lua 5.1 という本件の環境では、先述の通り _
($\textrm{0x5F}$)の行だけはダメ文字に該当しない。
あと、ダメ文字資料ではないが、Shift_JIS の全ての文字コードを一望したければこちら。
→ 文字コード表 シフトJIS(Shift_JIS)
理解して表をたどれば、ダメ文字も自然と判明する。
ぼやき:ダメ文字そのものについて
「ダメ文字」という概念はどうやら、プログラミング言語や実行環境を超えて、Shift_JIS のデータを扱う限り一定以上付いて回る問題らしい(特に PHP で)。
だからこそ「ダメ文字」という汎用的な命名もされているし、上記のような Lua と全く無関係なサイトでも情報が見つかるんだろう。
そして、そうは言っても言語や環境によって記号扱いされる文字は異なる、という点もまたややこしい。
特にエスケープ文字やパス区切り文字として名高い \
($\textrm{0x5C}$)が最も問題になりやすいらしく、専用に「5C 問題」と呼ばれているとか。実際、本件でも見事ダメ文字に含まれている。
本件で唯一セーフだった記号 _
も、別の言語や環境ではダメ文字になることがあるのかもしれない。
対策
以上より、AviUtl スクリプトを書く際のダメ文字対策として導き出される結論は――
ダメ文字にあたる全角カタカナ・全角漢字・全角記号をよく把握しておき、それらを避けるようにして全角生キーを命名しましょう!
$\def\sp{\hspace{1.5em}} \Large \hspace{1.5em} \mathbf{で \sp は \sp な \sp く}$
全角生キーをやめましょう!!!!
a = {ten = 10} -- 全角キーをやめるか、
a = {["十"] = 10} -- 生キーをやめる。
最初から安全なやり方でやっときゃヨシ!!
少なくとも、["
"]
でくくるだけでダメ文字なんて容易に潰せるのであった。
Lua くんも AviUtl くんも、既によくやってくれてるよ…!
(でも正直、AviUtl くんには Shift_JIS を捨てて UTF-8 に移行してほしいな………)
おわり
-
Lua はてっきり 5.3 が最新だと思ってたんだけど、ここで書くために調べたらつい1ヶ月ほど前に 5.4 が来てた。めでたい。 ↩
-
別の環境ではきっと違うと思う。例えば、https://www.lua.org/demo.html で全角生キーをやると普通にエラーが出る。 ↩
-
見やすさのために、本記事では $\mathrm{\LaTeX}$ の力を借りて十六進数を $\textrm{0x00}$ ように整形表示したい。 ↩