Delphi のソースコードを一括で UTF-8 に変換する

# はじめに
Delphi のソースファイルはデフォルト ANSI コードページが使われるので、海外の方が作ったアプリのソースファイルを開くとソースコードが破壊される事があります。

また、自身で書いたコードもデフォルトでは Shift_JIS (コードページ: 932) で保存されている事になります (日本語環境をお使いの場合)。

Delphi 2005 以降 (正確には Delphi 8 以降)、ソースファイルを UTF-8 で保存する事が可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/2af09797-e804-202b-a34e-abbd0588cb2a.png)
古い IDE とファイルを共有するのでもない限り、エンコーディングを Shift_JIS にする必然性はありません。

## デフォルトのファイルエンコード

デフォルトのファイルエンコードを UTF-8 に変更するにはいくつか方法があります。

### 1. Delphi 10.4 Sydney 以降

[ツール | オプション...] でオプションダイアログを開き [ユーザーインターフェイス | エディタ] でデフォルトのファイルエンコードを指定する事ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/9f08ab27-7940-0ca9-4eb7-73a0f5a29c7b.png)

### 2. UTF8ize Plugin (Delphi 2010 ～ 10.3 Rio)

**UTF8ize Plugin** を導入すると、新規に作成したり開いたユニットのファイルエンコードが UTF-8 にセットされます。

 - [UTF8ize Plugin。(Swanman's Horizon)](https://lyna.hateblo.jp/entry/20150618/1434639803)
 - [自作プラグインの 10.3 Rio 対応状況。(Swanman's Horizon)](https://lyna.hateblo.jp/entry/20181123/1542962830) - UTF8ize plugin Ver.0.0.5 があります。

先述の通り、10.4 Sydney 以降ではオプションダイアログでデフォルトのファイルエンコードを変更できますので、UTF8ize が必要になるのは Delphi 2010 ～ 10.3 Rio という事になるかと思います。

### 3. レジストリ
Delphi 8 以降のガリレオ IDE では、レジストリを変更する事によって、デフォルトのファイルエンコードを UTF-8 に変更できます。

| Delphi | BDS バージョン | レジストリキー  |
|:---|:---|:---|
| Delphi 8, 2005, 2006, 2007 | 2.0, 3.0, 4.0, 5.0  | HKEY_CURRENT_USER\Software\Borland |
| Delphi 2009, 2010  | 6.0, 7.0  | HKEY_CURRENT_USER\Software\CodeGear |
| Delphi XE 以降  | 8.0 ～  | HKEY_CURRENT_USER\Software\Embarcadero |

いずれかの、`BDS\xx.x\Editor` (xx.x は BDS バージョン) にある (なければ新規作成)  `DefaultFileFilter` (REG_SZ) の値を `Borland.FileFilter.ANSI` から `Borland.FileFilter.UTF8ToUTF8` に変更します。

例えば Delphi 2007 の場合、`HKEY_CURRENT_USER\Software\Borland\BDS\5.0\Editor` に `DefaultFileFilter` を新規作成してデータを `Borland.FileFilter.UTF8ToUTF8` に設定します [^1]。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/a2eda785-ac47-a99f-43ed-0b1923ce94a3.png)

**See also:**

 - [More IDE secrets - UTF8 and the Editor (community.embarcadero.com)](https://community.embarcadero.com/blogs/entry/more-ide-secrets--utf8-and-the-editor-284)

# ANSI2UTF8

Delphi のソースコードのコードページを一括で UTF-8 に変換するツールを Delphi で書いてみます。

## ソースコード

Delphi 10.3 Rio 以降でコンパイルできますが、インライン変数宣言を使わなければ Delphi XE とかでもコンパイルできると思います。

```pascal:ANSI2UTF8.dpr
program ANSI2UTF8;

{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.Classes, System.IOUtils, System.StrUtils, System.WideStrUtils;

const
  SearchOption: array [Boolean] of TSearchOption =
    (TSearchOption.soTopDirectoryOnly, TSearchOption.soAllDirectories);

begin
  var Dir := '';
  var Enc: TEncoding := nil;
  var ErrFlg := False;
  var IsBackup := False;
  var IsRecursive := False;

  Writeln('ANSI to UTF-8 File Converter - Copyright (c) 2021 DEKO');
  repeat
    if ParamCount = 0 then
      begin
        ErrFlg := True;
        Break;
      end;
    IsBackup    := FindCmdLineSwitch('b', True);
    IsRecursive := FindCmdLineSwitch('r', True);
    var sCodePage := '';
    var IsCodePage  := FindCmdLineSwitch('c', sCodePage);
    if IsCodePage then
      begin
        var CodePage := StrToIntDef(sCodePage, -1);
        if CodePage = -1 then
          begin
            Writeln('Invalid Codepage.');
            ErrFlg := True;
            Break;
          end
        else
          Enc := TEncoding.GetEncoding(CodePage);
      end;
    Dir := ExpandFileName(ParamStr(ParamCount));
    if not TDirectory.Exists(Dir) then
      begin
        Writeln('Invalid Path.');
        ErrFlg := True;
        Break;
      end;
  until True;

  if ErrFlg then
    begin
      Writeln(#$0D#$0A'Usage: ' + TPath.GetFileNameWithoutExtension(ParamStr(0)) + ' [-B] [-C CodePage] [-R] ProjectDir');
      Writeln('');
      Writeln('  -B'#$09#$09'Create Backup File');
      Writeln('  -C CodePage'#$09'Set CodePage');
      Writeln('  -R'#$09#$09'Recursive');
      ExitCode := -1;
      Exit;
    end;

  ExitCode := 1;

  if Enc = nil then
    Enc := TEncoding.GetEncoding(0);

  var MS := TMemoryStream.Create;
  var SL := TStringList.Create;
  try
    Writeln('');
    for var FileName in TDirectory.GetFiles(Dir, '*.*', SearchOption[IsRecursive]) do
      begin
        var Ext := LowerCase(TPath.GetExtension(FileName));
        if not MatchStr(Ext, ['.pas', '.inc', '.dpr', '.dpk']) then
          Continue;
        MS.LoadFromFile(FileName);
        if not HasUTF8BOM(MS) then
          begin
            MS.Position := 0;
            SL.LoadFromStream(MS, Enc);
            SL.SaveToFile(FileName, TEncoding.UTF8);
            if IsBackup then
              begin
                MS.Position := 0;
                MS.SaveToFile(FileName + '.bak');
              end;
            Writeln(FileName);
          end;
      end;
  finally
    SL.Free;
    MS.Free;
    Enc.Free;
  end;

  ExitCode := 0;
end.
```

C++ Builder にも対応させたい場合には、`MatchStr()` の所に、変換対象となるソースファイルの拡張子を追加してください。

```pascal:ANSI2UTF8.dpr
        if not MatchStr(Ext, ['.pas', '.inc', '.dpr', '.dpk',
                              '.c'  , '.h'  , '.cpp', '.hpp']) then
          Continue;
```

何か漏れているような...？


## 使い方

`ANSI2UTF8.EXE` にプロジェクトフォルダを指定して実行します。

```text
ANSI to UTF-8 File Converter - Copyright (c) 2021 DEKO

Usage: ANSI2UTF8 [-B] [-C CodePage] [-R] ProjectDir

  -B            Create Backup File
  -C CodePage   Set CodePage
  -R            Recursive
```

| フラグ | 意味 |
|:---|:---|
| -B | 変換前のファイルを .BAK として残します |
| -C CodePage | 変換対象ファイルのコードページを指定します  |
| -R | サブフォルダ内のファイルも変換します |

普通の使い方は、

```text
ANSI2UTF8 <ProjectDir>
```

です。

サブフォルダにもソースファイルがある場合には `-R` を、海外の方が作られたプロジェクトファイルを日本語環境で変換する場合には `-C 1252` 等を指定してください [^2]。

**See also:**

 - [コードページ識別子 (docs.microsoft.com)](https://docs.microsoft.com/ja-jp/windows/win32/intl/code-page-identifiers)


# おわりに

このツールは未公開だと思って Qiita に記事を書いたのですが、やっぱり公開してました。がっでむ。

 - [ソースコードをすべて UTF-8 にする (Delphi Forum)](https://ht-deko.com/delphiforum/?vasthtmlaction=viewtopic&t=1608.0)

折角なので、整理したソースコードを反映させておきました。

[^1]: Qiita の記事からコピペすると末尾にスペースが含まれがちなので注意してください。
[^2]: 何語で書かれているのかを調べ、適切なコードページを指定する必要があります。