Qiitaでバッチファイルの謎なシンタクスハイライトを直したい

# はじめに

筆者は Windows でバッチファイルを愛用しているが，そのバッチファイルを Qiita の記事内で取り上げるときにシンタクスハイライトが気持ち悪いので下記の記事で取り上げたことがある。

[Qiitaでバッチファイルのシンタクスハイライトが謎な件 - Qiita](https://qiita.com/TETSURO1999/items/6e13356babbd8824b3ed)

具体的には下記のような感じである。なおシンタクスハイライト自体はバージョンアップによって変更される可能性があるため，この記事を書き始めた時点（2024年11月現在）における色付けを保存したものを以下に示す。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
title {
  margin 0
  padding 0 16 16 16
  roundCorner 16
  backgroundColor #202020
  fontColor :#e3e3e3
  horizontalAlignment left
  fontStyle normal
  fontName monospaced
}
</style>
sprite TITLE <svg width="692" height="32">
<path d="M0 0 v28 a4 4 0 0 0 4 4 h100 a4 4 0 0 0 4 -4 v-28" fill="#606060">
<text x="8" y="24" font-size="16" fill="white">BUILD.CMD</text>
</svg>
title
<$TITLE>

@echo <color:#8bdf4c>off</color>
setlocal
<color:#979797>rem −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−</color>
<color:#979797>rem csc.exe にパスが通っているかどうかの確認</color>
<color:#979797>rem −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−</color>
<color:#ebd247>set DOTNETPATH</color>=
<color:#ebd247>for</color> %%I <color:#ebd247>in</color> <color:#ff8095>(</color> csc<back:#901b1b>.exe</back> <color:#ff8095>)</color> <color:#ebd247>do set DOTNETPATH</color><color:#ff8095>=</color><back:#901b1b>%%</back><color:#ff8095>∼</color>$PATH:I
<color:#ebd247>if</color> <color:#ff8095>defined</color> <color:#ebd247>DOTNETPATH goto LETS_COMPILE</color>
<color:#979797>rem −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−</color>
<color:#979797>rem .NET Framework のインストールパスを探す</color>
<color:#979797>rem −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−</color>
<color:#ebd247>for</color> <color:#8bdf4c>/F</color> <color:#3bbfbf>"usebackq tokens=1,2,*"</color> %%I <color:#ebd247>in</color> <color:#ff8095>(</color>
  <color:#3bbfbf>`reg query "HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" /v InstallPath`</color>
<color:#ff8095>)</color> <color:#ebd247>do</color> <color:#ff8095>(</color>
  <color:#ebd247>if</color> <color:#3bbfbf>"</color>%%I<color:#3bbfbf>"</color><color:#ff8095>==</color><color:#3bbfbf>"InstallPath"</color> <color:#ebd247>set</color> <color:#3bbfbf>"DOTNETPATH=</color>%%K<color:#3bbfbf>"</color>
<color:#ff8095>)</color>
<color:#ebd247>if</color> <color:#ff8095>defined</color> <color:#ebd247>DOTNETPATH goto APPEND_PATH</color>
echo <color:#a980f5>^[</color><color:#ff8095>[</color><color:#a980f5>91</color><color:#ebd247>m</color>.NET <color:#ebd247>Framework</color> のインストールパスが見つかりません！
pause
<color:#ebd247>exit</color> <color:#8bdf4c>/b</color>
:APPEND_PATH
echo パスに %DOTNETPATH% を追加します。
<color:#ebd247>set</color> <color:#3bbfbf>"PATH=</color>%PATH%<color:#3bbfbf>;</color>%DOTNETPATH%<color:#3bbfbf>"</color>
:LETS_COMPILE
<color:#979797>rem −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−</color>
<color:#979797>rem コンパイル</color>
<color:#979797>rem −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−</color>
echo <color:#ebd247>evalbar</color>.cs をコンパイルします。
<color:#ebd247>if</color> <color:#ff8095>exist</color> <color:#ebd247>evalbar</color><back:#901b1b>.exe</back> del <color:#ebd247>evalbar</color><back:#901b1b>.exe</back>
<color:#ebd247>csc</color> <color:#8bdf4c>-o -w:</color><color:#a980f5>4</color> <color:#8bdf4c>-win</color><color:#a980f5>32</color><color:#ebd247>icon</color>:shogi.ico <color:#ebd247>evalbar</color>.cs
<color:#ebd247>if</color> <color:#ff8095>exist</color> <color:#ebd247>evalbar</color><back:#901b1b>.exe</back> <color:#ebd247>goto SUCCESS</color>
echo <color:#a980f5>^[</color><color:#ff8095>[</color><color:#a980f5>91</color><color:#ebd247>mevalbar</color> のコンパイルに失敗しました！
pause
<color:#ebd247>exit</color> <color:#8bdf4c>/b</color>
:SUCCESS
echo <color:#ebd247>evalbar</color> のコンパイルに成功しました。
pause
<color:#ebd247>exit</color> <color:#8bdf4c>/b</color>
end title
@enduml
```

違和感を感じる部分を以下列挙する。

- 実行ファイル名 `csc.exe`，`evalbar.exe` の `.exe` 部分がエラー判定となること。
- FOR 変数 `%%~$PATH:I` もエラー判定となること。
- キーワード属性 `#ebd247` の対象が謎。`echo` や `pause` が外されて `if`，`goto`，`exit`，`for`，`do`，`in` などの制御構文が入るのは分かるが，それ以外にも `echo` 文の単なる出力文字列や `goto` 文のラベルにまでマッチングしているのがおかしい。
- 環境変数の設定 `set` 文がキーワード属性 `#ebd247` になっているのは分かるが，環境変数まで対象になっているのは止めたい。
- オプションスイッチがアトリビュート属性 `#8bdf4c` になっているのは分かる。`-w:4` の部分は分かるが，`-win32icon:shogi.ico` の色付けは無理やり感がある。

# 開発環境の立ち上げ

この記事を書いている現在，Qiita のシンタクスハイライトは Ruby の rouge によって実現されているので，まず ruby のインストールから始める。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
title {
  padding 0 16 16 16
  roundCorner 16
  fontStyle normal
  horizontalAlignment left
  fontColor #cccccc
  backgroundColor 0c0c0c
  fontName monospaced
}
</style>
sprite TITLE <svg width="692" height="32">
<path d="M0 0 v28 a4 4 0 0 0 4 4 h164 a4 4 0 0 0 4 -4 v-28" fill="#606060">
<text x="8" y="24" font-size="16" fill="white">ruby のインストール</text>
</svg>
sprite BR <svg width="1" height="20"></svg>
title
<$TITLE>

c:\>winget install RubyInstallerTeam.Ruby.3.2
見つかりました <color:cyan>Ruby 3.2</color> [<color:cyan>RubyInstallerTeam.Ruby.3.2</color>] バージョン 3.2.1-1<$BR>
このアプリケーションは所有者からライセンス供与されます。<$BR>
Microsoft はサードパーティのパッケージに対して責任を負わず、ライセンスも付与しません。<$BR>
ダウンロード中 <color:#3b78ff>https://github.com/oneclick/rubyinstaller2/</color><$BR>
<color:#3b78ff>releases/download/RubyInstaller-3.2.1-1/rubyinstaller-3.2.1-1-x64.exe</color><$BR>
  <back:#002871><color:#3b78ff>······························</color></back>  14.2 MB / 14.2 MB<$BR>
インストーラーハッシュが正常に検証されました<$BR>
パッケージのインストールを開始しています...<$BR>
インストールが完了しました<$BR>
end title
@enduml
```

次に gem を使って rouge をインストールする。

```plantuml
@startuml
<style>
root {
  margin 0
  padding 0
}
title {
  padding 0 16 16 16
  roundCorner 16
  fontStyle normal
  horizontalAlignment left
  fontColor #cccccc
  backgroundColor 0c0c0c
  fontName monospaced
}
</style>
sprite TITLE <svg width="692" height="32">
<path d="M0 0 v28 a4 4 0 0 0 4 4 h172 a4 4 0 0 0 4 -4 v-28" fill="#606060">
<text x="8" y="24" font-size="16" fill="white">rouge のインストール</text>
</svg>
sprite BR <svg width="1" height="20"></svg>
title
<$TITLE>

c:\>gem install rouge<$BR>
Fetching rouge-4.5.1.gem<$BR>
Successfully installed rouge-4.5.1<$BR>
Parsing documentation for rouge-4.5.1<$BR>
Installing ri documentation for rouge-4.5.1<$BR>
Done installing documentation for rouge after 4 seconds<$BR>
1 gem installed<$BR>
end title
@enduml
```

# デバッグ用コード

改造のベースとするために `batchfile.rb` を開発用フォルダにコピーする。筆者の環境の場合，もともとのファイルは下記のパスにあった。

`C:\Ruby32-x64\lib\ruby\gems\3.2.0\gems\rouge-4.5.0\lib\rouge\lexers\batchfile.rb`

コピーする際にファイル名を `MyBatchfile.rb` に変え，クラス名も `MyBatchfile` に変える。

```ruby:MyBatchfile.rb
module Rouge
  module Lexers
    class MyBatchfile < RegexLexer
    # 以下略
```

デバッグモード用のスクリプトを以下に示す。先ほどコピーした `MyBatchfile.rb` と同じフォルダに置く。なお，入力するファイル（バッチファイル）の文字コードを `UTF-8` にする必要がある。

```ruby:debug.rb
require "rouge"
require_relative "MyBatchfile"
if ARGV.size == 0 then
  puts "usage: debug(.rb) [filename(.cmd/bat)]"
  exit
end
filename = ARGV[0]
if !File.exist?(filename) then
  puts "#{filename} is not found."
  exit
end
input = File.open(filename, "rt:BOM|utf-8")
source = input.read
Rouge::Lexer.enable_debug!
lexer = Rouge::Lexers::MyBatchfile.new({'debug'=>'1'})
null = Rouge::Formatters::Null.new
null.format(lexer.lex(source))
exit
```

デバッグモードの実行例を以下に示すが，なんといきなり `echo` コマンドがビルトインコマンドとして判定されておらず，単なるテキストとして誤判定されているではないか！ちなみにビルトインコマンドとして判定されていない理由は先頭に `@` の文字が付いているせいである。
※`@` を外せばビルトインコマンド判定になる。

```:デバッグモードの実行
c:\qiita\rouge>debug build.cmd
lexer: batchfile
stack: [:root]
stream: "@echo off\nsetlocal\nr"
  entering: mixin :basic
  trying: #<Rule /@?\brem\b.*$/i>
  trying: #<Rule /^::.*$/>
  trying: #<Rule /:[a-z]+/i>
  trying: #<Rule /([a-z]\w*)(\.exe|com|bat|cmd|msi)?/i>
  trying: #<Rule /((?:[\/\+]|--?)[a-z]+)\s*/i>
  entering: mixin :expansions
  trying: #<Rule /[%!]+([a-z_$@#]+)[%!]+/i>
  trying: #<Rule /(\%+~?[a-z]+\d?)/i>
  exiting: mixin :expansions
  trying: #<Rule /[<>&|(){}\[\]\-+=;,~?*]/>
  exiting: mixin :basic
  entering: mixin :data
  trying: #<Rule /\s+/>
  trying: #<Rule /0x[0-9a-f]+/i>
  trying: #<Rule /[0-9]/>
  trying: #<Rule /["]/>
  trying: #<Rule /[']/>
  trying: #<Rule /[`]/>
  trying: #<Rule /[^\s&|()\[\]{}\^=;!%+\-,"'`~?*]+/>
    got: "@echo"
    yielding: Text, "@echo"

～以下略～
```

長くなったので次回に続く。

https://qiita.com/TETSURO1999/items/fc6928568de9bdf15b45

# 参考文献

- [Lexer Development - github/rouge-ruby](https://github.com/rouge-ruby/rouge/blob/master/docs/LexerDevelopment.md)

- [rougeのlexer開発ワークフロー - HatenaBlog](https://gfx.hatenablog.com/entry/2017/06/03/154021)

- [Ruby/Rouge でコードをシンタックスハイライトした HTML と CSS を出力させる - HatenaBlog](https://obelisk.hatenablog.com/entry/2018/09/03/013400)