前提知識
FilesystemObjectで
CreateObject("Scripting.FileSystemObject").OpenTextFile filename, True, 0
とするとANSI形式のテキストファイルが作成される。
0 ASCIIファイルとして開きます※厳密にいうとCP932
-1 Unicodeファイルとして開きます※UTF-16LE
となっているが、このUNICODWはUTF-16LEである
UTF-16 (UCS/Unicode Transformation Format 16[注釈 1]) とは、UnicodeおよびISO/IEC 10646の、符号化フォームおよび符号化スキーム(文字符号化方式を参照)のひとつである。
https://docs.microsoft.com/ja-jp/cpp/text/support-for-unicode?view=vs-2019
一般的に使用されるほとんどすべての文字は、1つの16ビットワイド文字で UTF-16 で表されるため、ワイド文字を使用すると、国際文字セットを使用したプログラミングが簡単になります。 16LE (リトルエンディアン用) を使用してエンコードされたワイド文字は、Windows のネイティブ文字形式です。
Unicode ~UTF-8、UTF-16との違い~
UNICODEは文字集合UTF-8 UTF-16は符号化方式の違い。エンディアンは下位8ビットを先に並べるか、あとに並べるかの違い。
文字コードの考え方から理解するUnicodeとUTF-8の違い
エンディアンはBOMが必要になる。
なお、現在のNotepad.exeのデフォルトはUTF-8だがBOMなしである。
このため、Excelでは文字化けする。ExcelはBOMありUTF-8にしか対応していないため。
これは余談だがFilesystemObjectの仕様で異なる文字コードは書き込めない
FilesystemObjectは文字コードが違う文字列を書き込まない
Microsoft VBScript 実行時エラー: プロシージャの呼び出し、または引数が不正です。
Const ForReading = 1, ForWriting = 2, ForAppending = 8
Const TestString ="abcd"
Dim WSH : Set WSH = WScript.CreateObject("WScript.Shell")
Dim fs : Set fs = CreateObject("Scripting.FileSystemObject")
With fs
strFile = WSH.CurrentDirectory & "\" & "testFSAnsi.txt"
if .FileExists(strFile) Then .DeleteFile strFile
set ts = .OpenTextFile(strFile, ForWriting , True , 0)
ts.WriteLine TestString & vbCrLf
ts.Close
set ts = Nothing
End With
Set fs = Nothing : Set WSH = Nothing
WScript.Quit
いまから紹介するコードをバグらせることが可能である。
どうするのか。
- 上記のコードはANSIで保存されているが、これをコードをUTF-16LE(メモ帳ではUNICODE)で保存する
- `TestString ="abcd"`に絵文字や環境依存文字を入れる`Const TestString ="①💹 abcd"`
- 保存して起動する。
Const ForReading = 1, ForWriting = 2, ForAppending = 8
Const TestString ="①💹 abcd"
Dim WSH : Set WSH = WScript.CreateObject("WScript.Shell")
Dim fs : Set fs = CreateObject("Scripting.FileSystemObject")
With fs
strFile = fs.BuildPath(WSH.CurrentDirectory , "testFSAnsi.txt")
if .FileExists(strFile) Then .DeleteFile strFile
On Error Resume Next
set ts = .OpenTextFile(strFile, ForWriting , True , 0)
ts.Write TestString & VbCrlf
If err.Number <> 0 Then
Err.Clear
ts.Close
Else
ts.Close
End If
set ts = Nothing
End With
Set fs = Nothing : Set WSH = Nothing
WScript.Quit
ディアボロ…キング・クリムゾンでエラーが発生したという時を消し飛ばすつもりか!?
無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄
無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄*Tree(3)
0kB UTF-8のテキストで空白だ。
これで「指定した文字列をテキストファイルに書き込むという真実」に永遠に到達することができない。キング・クリムゾンのスタンド能力でも回避不能。「終わりのないのが終わり。」ゴールド・エクスペリエンス・レクイエムの攻撃。バグが起きるスクリプトに悩まされながら永遠に死に続ける。
原因はNotepad
FileSystemObjectはANSIに入らない文字はWriteLineで書き込もうとするとエラーが起きる。
ここで、普通のアルファベットなら当然書き込まれる。
次に、それをNotepadで開くことは普通にあると思われる。
ここでNotepadはUTF-8とANSIとどちらでも取れるときは、標準のUTF-8と判断するという問題が発動する。これを調整することもできない。
さらにUTF-8に強引に書き換わってしまうと、それからもとに戻らない
もっというと、文字を消去してUTF-8と解釈しうる状況になったとする。
そうするとANSIにしていてもUTF-8になってしまう。
おそらく、UTF-16LEで記述したほうがいいのかも知れない。ただ一歩間違うと機種依存文字をANSIに書き込もうとしてエラーになるなど、想定外の状況が起きる。
確認
とりあえず C:\hoge\hoge.vbsとする
この C:\hoge\がカレントディレクトリとなる
このc:\hogeに指定した文字列をいれたファイルを作成する
textファイルはFSではANSIであり、名前はtestFSAnsi.txt
また、このファイルは削除して再度作成される。
Const ForReading = 1, ForWriting = 2, ForAppending = 8
Const TestString ="abcd"
Dim WSH : Set WSH = WScript.CreateObject("WScript.Shell")
Dim fs : Set fs = CreateObject("Scripting.FileSystemObject")
With fs
strFile = fs.BuildPath(WSH.CurrentDirectory , "testFSAnsi.txt")
if .FileExists(strFile) Then .DeleteFile strFile
set ts = .OpenTextFile(strFile, ForWriting , True , 0)
ts.WriteLine TestString & vbCrLf
ts.Close
set ts = Nothing
End With
Set fs = Nothing : Set WSH = Nothing
WScript.Quit
たしかにアルファベットの段階では判定ができないため、これは致し方がない。
ここからさらに驚いた。
これを開いたまま、日本語を打ち込んでみた。
そして上書き保存をした。
まだUTF-8の表示だ。
閉じてNOTEPADで開いてみる
なんとUTF-8である。
つまり誤っているというより、最初判別がつかなければUTF-8になり、そのまま保存するとUTF-8になるということがわかった。
http://www.mermaid-tavern.com/pluto/tpc/tp3/tpc_0213.html
この人がなにかFilesystemObjectがUTF-8なんて作れるわけがないと、検索で来た人を罵倒しているのだが、自分も似たようなものなのでそれ自体は責めないが、思い込みとは怖いものである。
これはFileSystemObjectでANSIを指定していても書き換わってしまう。今までだと問題がなかったが、非常に危険だ。
なぜこれが危険かというとVBSCRIPT Schema.ini Batは基本的にANSIで書かないとエラーが起きるためだ。
回避方法
冒頭に全角の空白を1文字入れる。
もしこれができれば解決する。
Const ForReading = 1, ForWriting = 2, ForAppending = 8
Const TestString =" abcd"
Dim WSH : Set WSH = WScript.CreateObject("WScript.Shell")
Dim fs : Set fs = CreateObject("Scripting.FileSystemObject")
With fs
strFile = fs.BuildPath(WSH.CurrentDirectory , "testFSAnsi.txt")
if .FileExists(strFile) Then .DeleteFile strFile
set ts = .OpenTextFile(strFile, ForWriting , True , 0)
ts.Write TestString & VbCrlf
ts.Close
set ts = Nothing
End With
Set fs = Nothing : Set WSH = Nothing
WScript.Quit
しかし、この方法は問題がある
しかしコードにおいては全角の空白もエラーを起こしうる。
全角の空白を削除して保存する。
閉じて開いてみる
はい、UTF-8に早変わりです。
これってまずくないか
先日見ていた記事が出てこないのだが、たしか Windows Script Programmer 2017氏がAnswers MicrosoftでNotepad.exeのデフォルトをレジストリでANSIに変えられないか?と言っていた意味がわかった。これである。メモ帳の保存、文字コードのデフォルトがBOMなしUTF-8になった バッチファイル、コマンドスクリプトは注意
こうして書き換わってしまうことを問題視していると考えられる。
https://snow-white.cocolog-nifty.com/first/2019/05/post-7ab7a5.html
まあここではプログラムはないか
メモ帳の文字コード既定値がUTF-8に、Windows 10「May 2019 Update」
増田 裕正 富士ソフト
https://xtech.nikkei.com/atcl/nxt/column/18/00723/042300004/
この他、メモ帳で既存ファイルを開いた際、ファイルの文字コードと改行コードがステータスバーに表示されるようになった。どの文字コードや改行コードが利用されているかを一目で確認できる
あーこの人なにも見ずに書いているわけね。
どうせ自分がバグだと言っても聞く人はいないだろう。
レジストリを見てもANSIを矯正する方法がない
ここでポイントは、「どっちかわからないときはANSIにする」という設定がレジストリに必要だということだ。
ところが以下のような状況でひと目で分かるレジストリの値はなかった。
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Notepad\Capabilities
Application Discription
@%SystemRoot%\system32\NOTEPAD.EXE,-9
HKEY_LOCAL_MACHINE\SOFTWARE\RegisteredApplications
Notepad
Software\Microsoft\Windows\Notepad\Capabilities
HKEY_LOCAL_MACHINE\SOFTWARE\Synaptics\SynTPEnh\OSD\TouchPad\AppProfiles\Microsoft Notepad
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Classes\CLSID{1531d583-8375-4d3f-b5fb-d23bbd169f22}
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Low Rights\DragDrop{F41E8255-3897-4cf4-AEC7-4F85171A0B3C}
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Low Rights\ElevationPolicy{dc6bf185-7ae4-444e-8c35-e447b0d2bd1e}
この2つのポリシーだろうか。
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Low Rights\ElevationPolicy{dc6bf185-7ae4-444e-8c35-e447b0d2bd1e}
64bitだし既定のみで値の設定なし。
レジストリを検索してみたが、1発でデフォルトが決まっているのは判定できない。一番上の-9だろうか。
メモ帳の保存、文字コードのデフォルトがBOMなしUTF-8になった バッチファイル、コマンドスクリプトは注意
ここにレジストリの解決方法があるが、Notepadではなく、拡張子の編集のレジストリを変えると言うことだ。
HKEY_CLASSES_ROOT\batfile\shell\edit\command
の%SystemRoot%\System32\NOTEPAD.EXE %1
を%SystemRoot%\System32\NOTEPAD.EXE /A %1
batファイルはコマンドプロンプトを一時的に書き換える方法がある
バッチファイルを UTF-8 で書く
つまりVBSの他にbatファイルを作りchcp 65001をして、カレントを設定し、CscriptでVBSを起動する。
この方法であれば安全だ。
ただしBatを組まなければならない。
UTF-16LEが使えるか検討
なお、UTF-8とANSIがどちらでも解釈できる場合は、VBSCRIPTは動く。たとえばSchema.iniはフィールド名に日本語を使わないと、全て英数字のため、UTF-8ともANSIとも解釈できる。これは問題がない。VBSCRIPT自体はANSIで解釈しようとするためである。そこで日本語を追記すると、UTF-8に書き換わってしまうのである。
こうしたはNOTEPADの文字コードが書き換わる現象はUTF-16では生じない。
これはBOMがあるためだろう。
しかしUTF-16LEでコードを書くことを推奨しているケースはない。Schema.iniもANSIだ。
上記の絵文字を入れたコードは0を-1
に書き換えればよい。するとUTF-16LEになるため、書き込むことができる。
' このコードはメモ帳でいうUNICODE UTF-16LEで保存してください
Const ForReading = 1, ForWriting = 2, ForAppending = 8
Const TestString ="①💹 abcd"
Dim WSH : Set WSH = WScript.CreateObject("WScript.Shell")
Dim fs : Set fs = CreateObject("Scripting.FileSystemObject")
With fs
strFile = fs.BuildPath(WSH.CurrentDirectory , "testFSAnsi.txt")
if .FileExists(strFile) Then .DeleteFile strFile
On Error Resume Next
set ts = .OpenTextFile(strFile, ForWriting , True , -1)
ts.Write TestString & VbCrlf
If err.Number <> 0 Then
Err.Clear
ts.Close
Else
ts.Close
End If
set ts = Nothing
End With
Set fs = Nothing : Set WSH = Nothing
WScript.Quit
またwsfにする方法がある。
この時、wsfからVBSciptを呼び出す場合、その呼び出すvbsはUTF-16LEかANSIでありUTF-8では文字化けする
よく忘れるWSF(3)~文字エンコードはShift-JISかUTF-16 2012年05月21日
このタイトルの意味は、wsfはBOMつきUTF-8にできるが、呼び出すVBSはShift-Jis(ANSI)かUTF-16LEということである。
JScript(Windows スクリプト ファイル)をUTF-8で書く
ただし、ファイルを分割してインクルードすると文字コードのエンコードが上手く認識されないので、インクルードするファイルには BOM を付けて下さい。
インクルードが呼び出すという意味。つまりこれはVBSでもJScritでも同じであることがわかる。
<?XML version="version" standalone="yes" encoding="UTF-8"?>
<package>
<job>
<script language="VBScript">
<![CDATA[
Code
]]>
</script>
</job>
</package>
<?XML version="version" standalone="yes" encoding="UTF-8"?>
<package>
<job>
<script language="VBScript">
<![CDATA[
Const ForReading = 1, ForWriting = 2, ForAppending = 8
Const TestString ="①💹 abcd"
Dim WSH : Set WSH = WScript.CreateObject("WScript.Shell")
Dim fs : Set fs = CreateObject("Scripting.FileSystemObject")
With fs
strFile = fs.BuildPath(WSH.CurrentDirectory , "testFSAnsi.txt")
if .FileExists(strFile) Then .DeleteFile strFile
On Error Resume Next
set ts = .OpenTextFile(strFile, ForWriting , True , -1)
ts.Write TestString & VbCrlf
If err.Number <> 0 Then
Err.Clear
ts.Close
Else
ts.Close
End If
set ts = Nothing
End With
Set fs = Nothing : Set WSH = Nothing
WScript.Quit
]]>
</script>
</job>
</package>
ただ、このようにしても絵文字は出力に失敗する。
さらに呼び出す先がUTF-8だと失敗する。
しかし、UTF-8とANSIが安定しない場合に他のVBSや絵文字を使わなければ、一定の範囲でwsfが有効である。
セカンドベストとしてはありうると思われる。