Javaのコンパイルと実行を同時に行うバッチファイル

# はじめに
 javaの勉強で簡単なコードを書くためにわざわざEclipseを起動するのは面倒です。かといって、コンパイルと実行をコマンドで別々に行うのも面倒です。

そこで、コンパイルと実行を同時に行い、Pythonのように実行できるバッチフィルを作成しました。バッチファイルですのでWindows専用です。

## 実行例

テストコード（Main.java）

```java
/* Main.java */
/* UTF-8で保存 */
public class Main {
	public static void main(String[] args) {
		System.out.println("Hello java!");
		for(String arg : args){
			System.out.println(arg);  // 引数を表示
		}
	}
}
```

javac、javaコマンドを使って行う場合（2回コマンドを入力する必要があります）

```
>javac -encoding UTF-8 Main.java
>java Main hoge piyo
Hello java!
hoge
piyo
```

作成したバッチファイルを使う場合（1回でコンパイル・実行できます）

```
>javace -encoding UTF-8 Main.java hoge piyo
Hello java!
hoge
piyo
```

# ソースコード

```batchfile
@echo off

rem 遅延環境変数の展開を有効化
setlocal ENABLEDELAYEDEXPANSION

set selfname=%~n0

if "%1"=="" (
    call :usage
    exit /b
)

rem 変数の初期化
set javac_args=
set java_args=
set jflag=false
set cpflag=false
set cp_args=
set dflag=false
set classdir=./

rem javaソースファイルの前か後かを判定し、オプションや引数を取得
for %%i in (%*) do (
    set javacmp=%%~xi
    set tmparg=%%i
    if !javacmp!==.java (
        set javafile=%%i
        set jflag=true
    ) else (
        if !jflag!==false (
            call :func_javac_args
        ) else (
            set java_args=!java_args! %%i
        )
    )
)

rem javaファイルから拡張子（.java）を省いた名前を取得
set javarunfile=%javafile:~0,-5%
rem バックスラッシュはjavaコマンドで使えないため、スラッシュに変換
set javarunfile=%javarunfile:\=/%

rem classpathオプションを作成
set cp_opt=
if not "%cp_args%"=="" (
    set cp_opt=-classpath %cp_args%
)

rem javacを実行
javac %javac_args% %cp_opt% %javafile%
if errorlevel 1 (
    rem echo compile_error errorlevel:%errorlevel%
    exit /b
)

rem classファイルのあるディレクトリに移動
cd %classdir%

rem javaを実行
java %cp_opt% %javarunfile% %java_args%
if errorlevel 1 (
    rem echo run_error errorlevel:%errorlevel%
    exit /b
)

exit /b

rem -----サブルーチン-----
rem usage: 使い方を表示する
:usage
echo %selfname%: compile and execute java.
echo [Usage]
echo %selfname% [javac_args] {javafile}.java [java_args]
echo javac_args : optional arguments of 'javac'
echo java_args  : command-line arguments
exit /b

rem func_javac_args: javacの引数を処理する
:func_javac_args
set head=!tmparg:~0,1!
rem オプション判定
if "!head!"=="-" (
    rem classpathオプション判定
    if "!tmparg!"=="-classpath" (
        set cpflag=true
    ) else (
        if "!tmparg!"=="-cp" (
            set cpflag=true
        ) else (
            set cpflag=false
        )
    )
    rem -dオプションフラグ判定
    if "!tmparg!"=="-d" (
        set dflag=true
    )
)

rem -dオプションの引数を登録（javaコマンド実行時に使用）
if "!dflag!"=="true" (
    if not "!head!"=="-" (
        set classdir=!tmparg!
        set dflag=false
    )
)

rem classpath引数を登録
if "!cpflag!"=="true" (
    if not "!head!"=="-" (
        if "!cp_args!"=="" (
            set cp_args=!tmparg!
        ) else (
            set cp_args=!cp_args!;!tmparg!
        )
    )
) else (
    rem javacコマンドのオプションを登録
    set javac_args=!javac_args! !tmparg!
)

exit /b
```

# 使い方
## 事前準備

私の想定している使用方法です。

1. 上記コードをコピーして、メモ帳に貼り付ける
2. 文字コードに「ANSI」を指定し、「javace.bat」という名前で保存
3. そのファイルを、パスを通しても良い場所に移動（私は「C:/Users/ユーザー名/bat」というフォルダを作成し、その中に入れました）
4. そのフォルダのパスを通す

パスを通すのは任意です。ファイル名も特に決まりはありません。パスを通さない場合は実行時にバッチファイルのパスを指定するか、バッチファイルの存在するフォルダをカレントディレクトリに指定してください。

## 実行

コマンドプロンプトを起動して実行します。

```
javace hoge.java
```

上記のように、mainメソッドを含むjavaファイルを指定してください。コンパイルと実行が行われます。
すべての機能を使うには以下のように指定します。

```
javace [javacのオプション] ソースファイル.java [引数1, 引数2...]

使用例↓
javace -encoding UTF-8 -classpath .;../hoge -d ./bin pack/Main.java　arg1 arg2
```
`.java` ファイルより前が `javac`のオプション引数、後がjava実行時のコマンドライン引数です。

## 注意点

### javacコマンドのオプションについて

動作確認済みオプション

- -encoding
- -classpath (-cp)
- -d

上記以外のコマンドは動作確認していません。また、javaコマンドのオプションは使えません。

### ハイフンから始まるフォルダ名について

javacのオプションをハイフン（-）で判断しているため、ハイフンから始まるフォルダ名には対応していません（滅多にないと思いますが）。

### 文字コードについて

実行時に、

```
エラー: この文字(0x8B)は、エンコーディングwindows-31jにマップできません
```

のようなエラーが出る場合は、 `-encoding UTF-8` のように、文字コードのオプションを指定してください。

また、バッチファイルそのものの文字コードはShift-JISを想定しています。UTF-8で保存すると、バッチファイル内の日本語コメントでエラーが出る場合があります。

### -dオプションで動作しない場合について
-dオプションをつけずに実行した後、-dを指定すると実行できない場合があります。その場合、既に出力されているclassファイルを削除する必要があります。

例えば、

```
Main.java
bin (フォルダ)
mypack (フォルダ)
|-SayHello.java
```

という階層構造の場合、

```terminal
javace Main.java
```

として実行すると、

```
Main.java
Main.class
bin
mypack
|-SayHello.java
|-SayHello.class
```

のようにclassファイルが生成されます。この状態で

```terminal
javace -d ./bin Main.java
```

としても、binフォルダに`SayHello.class`は作成されません。この場合、mypackフォルダ内の`SayHello.class`を削除してから実行することで動作します。


## 動作確認環境

javac 15.0.2
java 15.0.2
Windows 10 (2004) 64bit
コマンドプロンプトのコードページ：932 (ANSI/OEM - 日本語 Shift-JIS)
バッチファイルの文字コード：Shift-JIS


# 解説

バッチファイルの簡単な解説をします。

### 冒頭
```batchfile
@REM javace.bat
@echo off

setlocal ENABLEDELAYEDEXPANSION

set selfname=%~n0

if "%1"=="" (
    call :usage
    exit /b
)
```

`setlocal ENABLEDELAYEDEXPANSION` は遅延環境変数の展開の有効化で、これをしないとforループで変数がまともに使えません。それと同時にsetlocalによって、このバッチファイル内で使用される変数はすべてローカル変数扱いとなるため、バッチファイルが終了すると変数の値は実行前に戻ります。

`selfname` 変数には実行したコマンド名（ここではjavace）を入れ、usageで使用しています。

if文は引数が0個の場合に使用方法を出力しています。usageラベルは後半に記述しています。

### 変数の初期化

```batchfile
rem 変数の初期化
set javac_args=
set java_args=
set jflag=false
set cpflag=false
set cp_args=
set dflag=false
set classdir=./
```

処理で使う変数を初期化しています。内訳は以下の通りです。

```
javac_args : javacのオプション（classpath以外）格納用
java_args  : java実行時のコマンドライン引数格納用
jflag      : .javaファイルを見つけたか
cpflag     : 現在の引数がclasspathオプションであるか
cp_args    : クラスパス格納用（セミコロン区切りで格納）
dflag      : 現在の引数がdオプションであるか
classdir   : classファイルのあるフォルダパス
```

引数関係はスペース区切りで上記変数に格納しなおします。また、クラスパスはセミコロン区切りで格納します。


### コマンド解析

```batchfile
@REM javace.bat
rem javaソースファイルの前か後かを判定し、オプションや引数を取得
for %%i in (%*) do (
    set javacmp=%%~xi
    set tmparg=%%i
    if !javacmp!==.java (
        set javafile=%%i
        set jflag=true
    ) else (
        if !jflag!==false (
            call :func_javac_args
        ) else (
            set java_args=!java_args! %%i
        )
    )
)
```

上のコードではコマンドの解析を行っています。 `%%i` にバッチファイルの引数を1つずつ格納しています。 はじめの `javacmp` はその引数の拡張子を抽出したもので、.javaファイルかどうかを判定するのに使用しています（[拡張子の抽出方法の参考](https://rainbow-engine.com/batch-get-filepath-filename/)）。.javaファイルを見つけると `javafile` 変数に格納し、 `jflag` をtrueにします（trueはただの文字列です）。これ以降に出現する引数はすべて実行時のコマンドライン引数となります。逆にこれ以前に出現する値はjavacのオプションとなります。

javacのオプション解析は `func_javac_args` というラベルをつけてまとめて書いているので、[後述](#javacオプション解析) します。

変数を `!` で囲んでいるのは遅延環境変数の展開における変数の使用方法だからです。

### 実行名を取得・classpathオプション作成
```batchfile
@REM javace.bat
rem javaファイルから拡張子（.java）を省いた名前を取得
set javarunfile=%javafile:~0,-5%
rem バックスラッシュはjavaコマンドで使えないため、スラッシュに変換
set javarunfile=%javarunfile:\=/%

rem classpathオプションを作成
set cp_opt=
if not "%cp_args%"=="" (
    set cp_opt=-classpath %cp_args%
)
```

最初の行は.javaファイル名から.javaを取り除いています。javaコマンド実行時はclassファイルの名前を指定するのでこのようにしています。その後、javaコマンドではバックスラッシュが使えないので、スラッシュに変換しています。

次に行っている文字列比較（ `if not "%cp_args%"==""` ）では、ダブルコーテーションで文字列を囲んでいますが、このようにしないと空文字判定ができません。また、文字列にスペースが含まれる場合も同様です（[参考](https://jj-blues.com/cms/wantto-comparestring/)）。

その後、クラスパスオプションのコマンドを作成しています。 `cp_args` にはクラスパス（例えば、 `.;../hoge` ）しか格納しないため、オプション名の  `-classpath` を付加しています。

### コマンド実行

```batchfile
@REM javace.bat
rem javacを実行
javac %javac_args% %cp_opt% %javafile%
if errorlevel 1 (
    rem echo compile_error errorlevel:%errorlevel%
    exit /b
)

rem classファイルのあるディレクトリに移動
cd %classdir%

rem javaを実行
java %cp_opt% %javarunfile% %java_args%
if errorlevel 1 (
    rem echo run_error errorlevel:%errorlevel%
    exit /b
)

exit /b
```

ここで各コマンドを実行しています。バッチファイルにおける実行には `call` や `start` （start /b /wait) がありますが、callは別のバッチファイルを呼び出す場合に使用されるものであるため（[callのドキュメント](https://docs.microsoft.com/ja-jp/windows-server/administration/windows-commands/call)） 不適です。また、 `start` を使うと別プロセスで走るみたいで、 `Ctrl + C` でjavaプログラムを停止させることができなくなります（[startのドキュメント](https://docs.microsoft.com/ja-jp/windows-server/administration/windows-commands/start)）。

`if errorlevel 1` は実行したコマンドの エラーレベルが1以上のときに入る処理、つまり異常が発生したときの処理です。 `if %errorlevel% neq 0` と書いても似たような挙動になりますが、上記のほうがよさそうです（[参考](http://blog.livedoor.jp/niwatori_atama/archives/51221352.html)）。

```bat
@REM バッチファイル
@REM errorlevelの比較
javac Main.java  <- コンパイルエラーが起きるとする
set errorlevel=0 <- errorlevel変数に0を代入
if %errorlevel% neq 0 echo エラー1 <- 表示されない
if errorlevel 1 echo エラー2 <- 表示される
```

javacまたはjavaコマンドで異常が発生した場合、そこで処理を終了するようにしています。（エラーレベルを表示する部分はコメントアウトしています。）（[ifのドキュメント](https://docs.microsoft.com/ja-jp/windows-server/administration/windows-commands/if)）

`exit /b` はバッチファイルを終了することを表しています。 `/b` オプションをつけないと実行しているコマンドプロンプトまで消えてしまいます。

### 使い方の表示

```batchfile
@REM javace.bat
rem usage: 使い方を表示する
:usage
echo %selfname%: compile and execute java.
echo [Usage]
echo %selfname% [javac_args] {javafile}.java [java_args]
echo javac_args : optional arguments of 'javac'
echo java_args  : command-line arguments
exit /b
```

ここは使い方を表示しているだけです。

### javacオプション解析

```batchfile
@REM javace.bat
:func_javac_args
set head=!tmparg:~0,1!
rem オプション判定
if "!head!"=="-" (
    rem classpathオプション判定
    if "!tmparg!"=="-classpath" (
        set cpflag=true
    ) else (
        if "!tmparg!"=="-cp" (
            set cpflag=true
        ) else (
            set cpflag=false
        )
    )
    rem -dオプションフラグ判定
    if "!tmparg!"=="-d" (
        set dflag=true
    )
)

rem -dオプションの引数を登録（javaコマンド実行時に使用）
if "!dflag!"=="true" (
    if not "!head!"=="-" (
        set classdir=!tmparg!
        set dflag=false
    )
)

rem classpath引数を登録
if "!cpflag!"=="true" (
    if not "!head!"=="-" (
        if "!cp_args!"=="" (
            set cp_args=!tmparg!
        ) else (
            set cp_args=!cp_args!;!tmparg!
        )
    )
) else (
    rem javacコマンドのオプションを登録
    set javac_args=!javac_args! !tmparg!
)
exit /b
```

javacのオプションを解析しています。最初のif文ではclasspathオプションか、dオプションかどうかの判定を行っています。クラスパスの処理を分けているのは、クラスパスの区切りにセミコロン（;）が使われているからです。コマンドプロンプトではスペース、イコール、セミコロンが区切り文字となるため、他と同じように処理するとクラスパスがばらばらになってしまいます。

ここでは、例えば、

```
javace -encoding UTF-8 -cp hoge/piyo;foo/bar fuga.java
```

のようなコマンドを打った場合、

```
-cp
hoge/piyo
foo/bar
```

の3つの引数を受けとっている間、  `cpflag` がtrueとなります。

2つ目の大きなif文では、dオプションで取得したclassファイル出力先パスをセットしています。取得したパスはjavaコマンド実行前にディレクトリ移動するのに使用しています。

最後のif文では、 `cpflag` がtrueのうち、オプション名（前にハイフンがついている）以外の引数をセミコロン区切りで結合しています。 `if "!cp_args!"==""` という判定については、これを入れないと先頭に不要なセミコロンがついてしまうからです。

```
;hoge/piyo;foo/bar <-不要なセミコロン
hoge/piyo;foo/bar
```

クラスパス以外の引数はスペース区切りで結合しています。こちらも先頭に不要なスペースが入りますが、スペースは動作に影響しないので特に対処はしていません。

## コマンド動作例

以下のコマンドを打った場合、

```
javace -d ../bin -encoding UTF-8 -classpath .;../hoge Main.java arg1 arg2
```

次のコマンドが実行されます。

```
javac -d ../bin -encoding UTF-8 -classpath .;../hoge Main.java
cd ../bin
java -classpath .;../hoge Main arg1 arg2
```

# 終わりに
実行コマンドが2回から1回に減るだけですが、複数回実行するときは前回に実行したコマンドを1つ呼び出すだけで動くので非常に楽です。

バッチファイルをこれだけ書いたのは初めてですので、無駄なことやおかしなことをしているかもしれません。バッチファイルは難しいですね。

## 余談

javaファイル1つだけならjavaコマンドを使ってコンパイルせずに実行できるそうです。
[【Java11】javacでコンパイルせずにJavaを実行してみた](https://qiita.com/yaaka4/items/344f0803139e83519c20)

ちなみにjavaにはjshellという対話形式の実行環境もあります。