UTF-8のBOM

JavaコンパイラのBOMエラー

BOMについて

BOM(Byte Order Mark)は、Unicode符号化のエンディアンを識別するために、 ファイルの先頭に付ける2バイトのコード(0xFE 0xFF)です。 UTF-8ではエンディアンの識別が不要なので、この2バイトのBOMを付けません。 その代わりにUTF-8であることを明示する3バイトのコード(0xEF 0xBB 0xBF)を ファイルの先頭に付けることがあり、これがBOM付きUTF-8ファイルです。 この3バイトのBOMが無いものをUTF-8Nと呼ぶこともあります。

Javaソースをメモ帳で編集してUTF-8で保存すると、選択の余地なく勝手にBOMが付いてしまい、 そのままコンパイルするとエラーになります。 Javaコンパイラは3バイトのBOMを解釈しないようです。

Javaコンパイラ(javac)が、次のようなエラーを表示した場合はBOMが原因です。

エラーメッセージ  XXXXXX.java:1: エラー: \65279は不正な文字です
  ?import java.io.IOException;

数値65279(0xFEFF)が不正だということは、2バイトのBOMを判定しようとしているのでしょうか。
サクラエディタ(フリー)や秀丸エディタ(シェアウェア)のように、 BOM有無を選べるエディタがあれば、BOM付きファイルを開いてBOM無しで保存するだけで問題解決です。

そういうエディタを持っていないしインストールする気もない、 または、『ファイルの先頭3バイトを削るくらい自分でプログラムを作ればいいじゃないか』 という人には、この後の記述が参考になると思います。

【Javaデータ型について】

Javaでは数値の型は必ず符号付きになります。
unsigned charみたいな、符号無し8ビットの型は無いようです。
byte型は8ビットですが、符号付き(-128~127)です。
char型は符号無しですが、8ビットではなく16ビットなので、バイト単位の扱いは注意が必要です。




2013年6月

とりあえずBOMを取り除く

作業フォルダにBOM付きファイルをコピーしておきます。 同じ場所に、BOM削除プログラムをShift-JISで作成します。
コマンドプロンプトで、コンパイルしてからクラスファイルを実行し、 BOMを取り除いたファイルを作成します。

作業フォルダ    :C:\tmp
BOM付きファイル :test01.java
プログラムソース:BomCut.java

BOM削除プログラム

プログラムの引数は、BOM付きファイル名だけです。 このファイルを読み込んで、一時ファイル(tmpsrc.tmp)に書き出します。 先頭3バイトがBOMの場合は4バイト目から、BOMでなければ1バイト目から 最終バイトまで出力します。 最後にBOM付きファイル名をバックアップ名(末尾に.bk)に変えて保存し、 一時ファイル名を元のBOM付きファイル名に変更します。

BomCut.java 【Shift-JIS(ANSI)】 (C:\tmp)
import java.io.*;

public class BomCut {
    public static void main(String[] args) {
        int iRet = 1;

        if ( args.length < 1 ) {
            System.out.println("## Error ## usage: java BomCut {I/Oファイル名}");
            System.exit(iRet);
        }
        String fnm = args[0];
        String onm = "tmpsrc.tmp";

        try{
            byte buf[] = new byte[1024];
            int ibf[] = new int[3];
            int c;

            File fli = new File(fnm);
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fli));

            if ( ( c = bis.read(buf, 0, 3) ) == -1 ) {
                System.out.println("## Error ## ファイルが読めません");
                bis.close();
                System.exit(iRet);
            }
            ibf[0] = (int)buf[0] & 0xff;
            ibf[1] = (int)buf[1] & 0xff;
            ibf[2] = (int)buf[2] & 0xff;

            File flo = new File(onm);
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(flo));

            if ( ibf[0] != 0xef || ibf[1] != 0xbb || ibf[2] != 0xbf ) {
                bos.write(buf, 0, c);
            }

            while ( ( c = bis.read(buf, 0, 1024) ) != -1 ) {
                bos.write(buf, 0, c);
            }
            bis.close();
            bos.close();

            File fl = new File(fnm + ".bk");
            fl.delete();
            if ( fli.renameTo(fl) ) {
                flo.renameTo(fli);
            }
            else {
                System.out.println("## Error ## ファイルが書き換えられません");
                System.exit(iRet);
            }
            iRet = 0;

        }catch(IOException e){
            System.out.println(e + "例外が発生しました");
        }
        System.exit(iRet);
    }
}

コンパイルと実行

コマンドプロンプトを起動し、作業フォルダに移動してコンパイル・実行・確認を行います。

C:\~ > cd \tmp   ENTER 
C:\tmp> javac BomCut.java   ENTER 
C:\tmp> java -classpath . BomCut test01.java   ENTER 
C:\tmp> dir   ENTER 

元のファイル(test01.java)のサイズが、バックアップファイル(test01.java.bk)より、 3バイト小さくなっているはずです。


プログラムとバッチを使って

ここではプログラムとバッチを使ってBOMを削除します。 プログラムは単純な入出力だけ行って、バッチでその他のファイル操作をします。
まず、作業フォルダにBOM付きファイルをコピーしておきます。 そこに、バッチファイルとBOM削除プログラムを作成します。 コマンドプロンプトで、プログラムをコンパイルしてからバッチを実行し、 BOMを取り除いたファイルを作成します。

作業フォルダ    :C:\tmp
BOM付きファイル :test01.java
プログラムソース:BomCut2.java
バッチファイル :jbmct.bat

BOM削除プログラム

プログラムの引数は、入力(BOM付き)ファイル名と出力ファイル名です。 入力ファイルを読み込んで、 先頭3バイトがBOMの場合は4バイト目から、BOMでなければ1バイト目から 最終バイトまで出力ファイルに書き出します。

BomCut2.java 【Shift-JIS(ANSI)】 (C:\tmp)
import java.io.*;

public class BomCut2 {
    public static void main(String[] args) {
        int iRet = 1;

        if ( args.length < 2 ) {
            System.out.println("## Error ## usage: java BomCut2 入力ファイル名 出力ファイル名");
            System.exit(iRet);
        }
        String fnm = args[0];
        String onm = args[1];

        try{
            byte buf[] = new byte[1024];
            int ibf[] = new int[3];
            int c;

            File fli = new File(fnm);
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fli));

            if ( ( c = bis.read(buf, 0, 3) ) == -1 ) {
                System.out.println("## Error ## ファイルが読めません");
                bis.close();
                System.exit(iRet);
            }
            ibf[0] = (int)buf[0] & 0xff;
            ibf[1] = (int)buf[1] & 0xff;
            ibf[2] = (int)buf[2] & 0xff;

            File flo = new File(onm);
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(flo));

            if ( ibf[0] != 0xef || ibf[1] != 0xbb || ibf[2] != 0xbf ) {
                bos.write(buf, 0, c);
            }

            while ( ( c = bis.read(buf, 0, 1024) ) != -1 ) {
                bos.write(buf, 0, c);
            }
            bis.close();
            bos.close();
            iRet = 0;

        }catch(IOException e){
            System.out.println(e + "例外が発生しました");
        }
        System.exit(iRet);
    }
}

バッチファイル

バッチの引数は、BOM付きファイル名だけです。 このファイルをバックアップファイル(.bk)に上書き確認なしでコピーします。 コピーに失敗した場合は、プログラムを実行しないで終了します。 バックアップファイルを入力ファイル、コピー元のBOM付きファイルを出力ファイルに指定して、 BOM削除プログラム(BomCut2.class)を実行します。

jbmct.bat 【Shift-JIS(ANSI)】 (C:\tmp)
@echo off
copy /Y %1 %1.bk 1> nul
if ERRORLEVEL 1 goto ERR
java -classpath . BomCut2 %1.bk %1
goto END

:ERR
echo Copy Error
:END

コンパイルと実行

コマンドプロンプトを起動し、作業フォルダに移動してコンパイル・実行・確認を行います。

C:\~ > cd \tmp   ENTER 
C:\tmp> javac BomCut2.java   ENTER 
C:\tmp> jbmct test01.java   ENTER 
C:\tmp> dir   ENTER 

元のファイル(test01.java)のサイズが、バックアップファイル(test01.java.bk)より 3バイト小さくなっているはずです。


C言語でチョロチョロっと

ファイルの頭3バイトを削るくらいC言語なら楽勝です。 ただ、Linuxと違ってWindowsにはCコンパイラが標準で装備されていません。
ここでは、「Visual Studio コマンド プロンプト」が使える場合と、 Cコンパイラがない場合のBOM削除を試みます。

Visual Studio コマンド プロンプトがある場合

マイクロソフトの無料開発環境(Microsoft Visual Studio 2010 Express)の C++(Microsoft Visual C++ 2010 Express)をインストールすると、 コマンドプロンプト(Visual Studio コマンド プロンプト(2010))が ツールとして組み込まれます。 これを使ってC言語のコンパイルをします。
※2012以降は入っていないらしいので、 今のうちにVisual Studio 2010 Express Combo DVD(VS2010ExpressJPN.iso)をダウンロードして、 インストールDVDを作成しておいたほうがいいかもしれません。

プログラムの内容は、前述の『プログラムとバッチを使って』と同じです。
まず、作業フォルダにBOM付きファイルをコピーしておきます。 そこにバッチファイルとBOM削除プログラムを作成します。 Visual Studio コマンドプロンプトで、プログラムをコンパイルしてからバッチを実行し、 BOMを取り除いたファイルを作成します。

作業フォルダ    :C:\tmp
BOM付きファイル :test01.java
プログラムソース:bomcuts.c
バッチファイル :bmcts.bat

BOM削除プログラム

プログラムの引数は、入力(BOM付き)ファイル名と出力ファイル名です。 入力ファイルを読み込んで、 先頭3バイトがBOMの場合は4バイト目から、BOMでなければ1バイト目から 最終バイトまで出力ファイルに書き出します。
ファイルアクセスには、ストリーム・ファイル入出力関数を使用しています。 BOMのチェックをするため、char(1バイト)を3個だけ最初に読み込みます。 データ出力(fwrite)のエラーチェックは面倒なので省略しました。

bomcutS.c 【Shift-JIS(ANSI)】 (C:\tmp)
#include <stdio.h>
#include <stdlib.h>
#include <io.h>
#include <fcntl.h>
#include <sys\types.h>
#include <sys\stat.h>
#include <errno.h>
#include <string.h>

main(int argc, char *argv[])
{
   FILE *fpi,*fpo;
   unsigned char buf[1024];
   int isz;

   if (argc < 3) {
      printf("usage: bomcuts in-file out-file\n");
      exit(1);
   }
   if ((fpi = fopen(argv[1], "rb")) == NULL) {
      printf("file open error %d:%s (%s)\n", errno, strerror(errno), argv[1]);
      exit(1);
   }
   if ((fpo = fopen(argv[2], "wb")) == NULL) {
      printf("file open error %d:%s (%s)\n", errno, strerror(errno), argv[2]);
      fclose(fpi);
      exit(1);
   }
   if ((isz = fread(buf, 1, 3, fpi)) <= 0) {
      printf("file read error %d:%s\n", errno, strerror(errno));
      if (isz == 0 ) {
         printf("nothing data (%s)\n", argv[1]);
      }
      fclose(fpi);
      fclose(fpo);
      exit(1);
   }
   if (buf[0] != 0xef || buf[1] != 0xbb || buf[2] != 0xbf) {
      fwrite(buf, 1, isz, fpo);
   }
   while ((isz = fread(buf, 1, sizeof(buf), fpi)) > 0) {
      fwrite(buf, 1, isz, fpo);
   }
   fclose(fpi);
   fclose(fpo);
   exit(0);
}

バッチファイル

バッチの引数は、BOM付きファイル名だけです。 このファイルをバックアップファイル(.bk)に上書き確認なしでコピーします。 コピーに失敗した場合は、プログラムを実行しないで終了します。 第1引数にバックアップファイル、第2引数にコピー元のBOM付きファイルを指定して、 BOM削除プログラム(bomcuts.exe)を実行します。

bmcts.bat 【Shift-JIS(ANSI)】 (C:\tmp)
@echo off
copy /Y %1 %1.bk 1> nul
if ERRORLEVEL 1 goto ERR1

bomcuts %1.bk %1
if ERRORLEVEL 1 goto ERR2
echo [%1] re-making completion
goto END

:ERR1
echo COPY failed
goto END

:ERR2
echo PROGRAM failed

:END

コンパイルと実行

Visual Studio コマンド プロンプトを起動し、 作業フォルダに移動してコンパイル・実行・確認を行います。

C:\~ > cd \tmp   ENTER 
C:\tmp> cl bomcuts.c   ENTER 
C:\tmp> bmcts test01.java   ENTER 
[test01.java] re-making completion
C:\tmp> dir   ENTER 

プログラムが正常終了したら、元のファイル(test01.java)のサイズが、 バックアップファイル(test01.java.bk)より3バイト小さくなっているはずです。


Cコンパイラがない場合

プログラムの内容は、前述の『Visual Studio コマンド プロンプトがある場合』と同じです。
まず、作業フォルダにBOM付きファイルをコピーしておきます。 そこにバッチファイルとBOM削除プログラムを作成します。 コマンドプロンプトで、プログラムをコンパイルしてからバッチを実行し、 BOMを取り除いたファイルを作成します。

作業フォルダ    :C:\tmp
BOM付きファイル :test01.java
プログラムソース:bomcutl.c
バッチファイル :bmctl.bat

Cコンパイラの準備

Cコンパイラとして、「LSI C-86 v3.30c 試食版」を使用します。 DOS用16ビット版ですが、小さなプログラムをコンパイルするには十分です。 下記のページからダウンロードできます。(64ビットのWindowsでは使えないらしいです)
【注意】 なにぶん古いコンパイラなので、なにか起こるかもしれません。ご使用は自己責任でお願いいたします。

「LSI C-86 v3.30c 試食版」ダウンロード   http://www.vector.co.jp/soft/dl/maker/lsi/se001169.html 

ダウンロードしたファイル(lsic330c.lzh)を解凍して、 フォルダ(lsic330c)を作業フォルダ(C:\tmp)に置きます。 それから、LSIC設定ファイルをメモ帳で開いて書き換えます。 ただし、コンパイルする時に毎回パラメータを指定するなら、 設定ファイルを書き換える必要はありません。

_LCC 【Shift-JIS(ANSI)】 (C:\tmp\lsic330c\BIN)
# LSI C-86 compiler's configuration file

-DLSI_C	
-XC:\tmp\lsic330c\BIN -LC:\tmp\lsic330c\LIB -IC:\tmp\lsic330c\INCLUDE -T -O
-acdos.obj $LSICOPTS
&	#Command line argument will be inserted here
-lknjlib -ldoslib -v

BOM削除プログラム

プログラムの引数は、入力(BOM付き)ファイル名と出力ファイル名です。 入力ファイルを読み込んで、 先頭3バイトがBOMの場合は4バイト目から、BOMでなければ1バイト目から 最終バイトまで出力ファイルに書き出します。 ファイルアクセスには、低水準ファイル入出力関数を使用してします。

bomcutL.c 【Shift-JIS(ANSI)】 (C:\tmp)
#include <stdio.h>
#include <stdlib.h>
#include <io.h>
#include <fcntl.h>
#include <sys\types.h>
#include <sys\stat.h>
#include <errno.h>
#include <string.h>

main(int argc, char *argv[])
{
   unsigned char buf[1024];
   int ifd,ofd,isz;

   if (argc < 3) {
      printf("usage: bomcutl in-file out-file\n");
      exit(1);
   }
   if ((ifd = open(argv[1], O_RDONLY|O_BINARY)) < 0) {
      printf("file open error %d:%s (%s)\n", errno, strerror(errno), argv[1]);
      exit(1);
   }
   if ((ofd = open(argv[2], O_WRONLY|O_TRUNC|O_BINARY)) < 0) {
      printf("file open error %d:%s (%s)\n", errno, strerror(errno), argv[2]);
      close(ifd);
      exit(1);
   }
   if ((isz = read(ifd, buf, 3)) <= 0) {
      printf("file read error %d:%s\n", errno, strerror(errno));
      if (isz == 0 ) {
         printf("nothing data (%s)\n", argv[1]);
      }
      close(ifd);
      close(ofd);
      exit(1);
   }
   if (buf[0] != 0xef || buf[1] != 0xbb || buf[2] != 0xbf) {
      write(ofd, buf, isz);
   }
   while ((isz = read(ifd, buf, sizeof(buf))) > 0) {
      write(ofd, buf, isz);
   }
   close(ifd);
   close(ofd);
   exit( 0 );
}

バッチファイル

バッチファイルを作成します。

bmctl.bat 【Shift-JIS(ANSI)】 (C:\tmp)
@echo off
copy /Y %1 jbk.jav 1> nul
if ERRORLEVEL 1 goto ERR1
echo dmy > jjj.jav

bomcutl jbk.jav jjj.jav
if ERRORLEVEL 1 goto ERR2

copy /Y jjj.jav %1 1> nul
if ERRORLEVEL 1 goto ERR1
del jjj.jav 1> nul 2> nul

copy /Y jbk.jav %1.bk 1> nul
if ERRORLEVEL 1 goto ERR1
del jbk.jav 1> nul 2> nul

echo [%1] re-making completion
goto END

:ERR1
echo COPY failed
goto END

:ERR2
echo PROGRAM failed

:END

古いファイルシステム(FAT)のコンパイラで作った実行ファイルを使うので、 ファイル名をルールに従った名前(8文字+拡張子3文字以内)にして、 プログラムの引数に指定します。 また、念のために出力先ファイルをダミーで作っておきます。

コンパイルと実行

コマンドプロンプトを起動して、最初にPATHを設定します。 この設定はコマンドプロンプトを閉じるまで有効です。

C:\~ > set path=%path%;c:\tmp\lsic330c\bin   ENTER 

作業フォルダに移動して、「LSI C-86 v3.30c 試食版」でコンパイルします。
C:\~ > cd \tmp   ENTER 
C:\tmp> lcc bomcutl.c   ENTER 
 【補足】 LSIC設定ファイル(_LCC)を書き換えなかった場合、 コンパイルは次のようになります。
lcc -Ic:\tmp\lsic330c\include -Lc:\tmp\lsic330c\lib bomcutl.c

Windows XPでは、マイクロソフトかな漢字変換(KKCFUNC)が組み込まれて、 コマンドプロンプト画面のリセット後にコンパイルが終了します。 Windows 7では、リセットに失敗してコマンドプロンプト画面の日本語が文字化けしますが、 コンパイルは正常に終了します。 どちらの場合も実行ファイル(BOMCUTL.EXE)が作られるので、これをバッチで実行します。

C:\tmp> bmctl test01.java   ENTER 
[test01.java] re-making completion
C:\tmp> dir   ENTER 

プログラムが正常終了したら、元のファイル(test01.java)のサイズが、 バックアップファイル(test01.java.bk)より3バイト小さくなっているはずです。

これでもいいのですが、余計な手間が掛かりすぎて、バッチが見苦しくなってしまいました。 解決策として、ファイルを直接使わずに リダイレクトで標準入出力にアクセスする方法を試してみます。

プログラムとバッチを書き直します。

書き直したプログラム

プログラムの引数はありません。 標準入力からデータを読み込んで、先頭3バイトがBOMでなければ終了します。 BOMの場合は4バイト目から最終バイトまで標準出力に書き出します。

bomcut.c 【Shift-JIS(ANSI)】 (C:\tmp)
#include <stdio.h>
#include <stdlib.h>
#include <io.h>
#include <errno.h>
#include <string.h>

main(int argc, char *argv[])
{
   unsigned char buf[1024];
   int isz;

   if ((isz = read(0, buf, 3)) <= 0) {
      sprintf(buf, "Error(%d)%s\r\n", errno, strerror(errno));
      if (isz == 0) {
         strcpy(buf, "nothing data\r\n");
      }
      write(2, buf, strlen(buf));
      exit(1);
   }
   if (buf[0] != 0xef || buf[1] != 0xbb || buf[2] != 0xbf) {
      exit(1);
   }
   while ((isz = read(0, buf, sizeof(buf))) > 0) {
      write(1, buf, isz);
   }
   exit(0);
}

バッチファイル作成

バッチファイルを作成します。
入力ファイルがBOMなしか、プログラムでエラーが発生した場合は、 コピーしたバックアップファイルを元に戻します。

bmct.bat 【Shift-JIS(ANSI)】 (C:\tmp)
@echo off
copy /Y %1 %1.bk 1> nul
if ERRORLEVEL 1 goto ERR1

bomcut <%1.bk >%1
if ERRORLEVEL 1 goto NCNG
echo [%1] re-making completion
goto END

:NCNG
copy /Y %1.bk %1 1> nul
del %1.bk
goto END

:ERR1
echo COPY failed

:END

リダイレクトについて
標準入力 : 0< 入力ファイル名 ( more < in.txt )
標準出力 : 1> 出力ファイル名 ( dir > out.txt )
標準エラー: 2> エラーファイル名 ( dir abc 2> err.txt )
エラー累積: 2>> 累積エラーファイル名 ( dir abc 2>> err.txt )
エラー破棄: 2> nul ( dir abc 2> nul )
同じ出力先: 1> 出力ファイル名 2>&1 ( dir abc 1> out.txt 2>&1 )

コンパイルと実行

作業フォルダのBOM付きファイル(test01.java)がBOM削除済みの場合は、 バックアップファイルをコピーするか、オリジナルを持ってきます。 また、コマンドプロンプトを閉じてしまった場合は、 もう一度起動してからPATHを設定する必要があります。

書き直したプログラムをコンパイルして実行します。

C:\tmp> lcc bomcut.c   ENTER 
C:\tmp> bmct test01.java   ENTER 
[test01.java] re-making completion
C:\tmp> dir   ENTER 

無事に終了して、元のファイル(test01.java)のサイズが、 バックアップファイル(test01.java.bk)より3バイト小さくなっているはずです。

もう一度、バッチ(bmct)を実行すると、バックアップファイルが消えます。 バッチの引数にBOM削除済みファイルを指定すると、プログラムが何もしないで異常終了(1)するので、 バッチは、元のファイルを復元して、不要になったバックアップファイルを削除します。

戻る