どうも、ラルフです

内容は表題の通り、動かないよってだけなので、少なくともこの記事では動作させるところまではできていません。

内部でどのような処理が行われているせいで起動できない っていうものの忘備録的な感じです。


Groovy は便利なもので、JREさえ入っていれば勝手に検索して起動してくれ、更にユーティリティクラスも結構あるという色々といい感じのものが入っています。

さらに、ポータブルに環境を持ち出すことも可能なので、動作させるものもJarにするより手軽に起動できます。

しかしながら、WindowsでGroovyが入っているフォルダ(親フォルダ含む)に ! (感嘆符) が入っていると起動できなくなるというのを報告で受けたので、それを調査してみました。

環境は、Windows 10 と Groovy 2.5.0-beta1 です。

Cドライブ直下に hoge! と言った感じのディレクトリを作り、その中にGroovyのバイナリと Groovyスクリプト、起動用batファイルを配備します。

1
println("Hello World")
1
2
3
4
5
6
7
8
cd /d "%~dp0"
if "%OS%"=="Windows_NT" setlocal

call groovy-2.5.0-beta-1\bin\groovy.bat Main.groovy
pause

if "%OS%"=="Windows_NT" endlocal
%COMSPEC% /C exit /B %ERRORLEVEL%
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
C:.
│  Main.groovy
│  run.bat
│
└─groovy-2.5.0-beta-1
    ├─bin
    ├─conf
    ├─embeddable
    ├─grooid
    ├─indy
    ├─lib
    └─licenses

こんな感じの構造で用意しています。

ここで、 run.bat を起動すると、

1
エラー: メイン・クラスorg.codehaus.groovy.tools.GroovyStarterが見つからなかったかロードできませんでした

というエラーで起動できません。

なお、ディレクトリ名から ! を取り除くと、正常に Hello World と出力されます。

原因を探っていきましょう。

Groovyを起動している際、環境変数 DEBUG に何らかの文字が入っていると起動スクリプト内の @echo off が実行されなくなるので、バッチファイルを編集します。

1
2
3
4
5
6
7
8
9
cd /d "%~dp0"
if "%OS%"=="Windows_NT" setlocal

set DEBUG=true
call groovy-2.5.0-beta-1\bin\groovy.bat Main.groovy
pause

if "%OS%"=="Windows_NT" endlocal
%COMSPEC% /C exit /B %ERRORLEVEL%

この状態で起動すると、Groovy の起動用バッチファイル内部での実行されているコマンドが出力されます。

出力結果からカレントディレクトリの表示などを消したバージョンが以下です。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
if "Windows_NT" == "Windows_NT" setlocal
set DIRNAME=C:\hoge!\groovy-2.5.0-beta-1\bin\
if "C:\hoge!\groovy-2.5.0-beta-1\bin\" == "" set DIRNAME=.\
"C:\hoge!\groovy-2.5.0-beta-1\bin\\startGroovy.bat" "C:\hoge!\groovy-2.5.0-beta-1\bin\" groovy.ui.GroovyMain Main.groovy
if "Windows_NT" == "Windows_NT" setlocal enabledelayedexpansion
set DIRNAME=C:\hoge!\groovy-2.5.0-beta-1\bin\
shift
set CLASS=groovy.ui.GroovyMain
shift
if exist "C:\Users\Ralph/.groovy/preinit.bat" call "C:\Users\Ralph/.groovy/preinit.bat"
set COMMAND_COM="cmd.exe"
if exist "C:\WINDOWS\system32\cmd.exe" set COMMAND_COM="C:\WINDOWS\system32\cmd.exe"
if exist "C:\WINDOWS\command.com" set COMMAND_COM="C:\WINDOWS\command.com"
set FIND_EXE="find.exe"
if exist "C:\WINDOWS\system32\find.exe" set FIND_EXE="C:\WINDOWS\system32\find.exe"
if exist "C:\WINDOWS\command\find.exe" set FIND_EXE="C:\WINDOWS\command\find.exe"
if not "C:\SDK\Java\jdk1.8.0_131" == "" goto have_JAVA_HOME
if "1" == "\" SET JAVA_HOME=C:\SDK\Java\jdk1.8.0_13
"C:\WINDOWS\system32\cmd.exe" /C DIR "C:\SDK\Java\jdk1.8.0_131"   2>&1  | "C:\WINDOWS\system32\find.exe" /I /C "C:\SDK\Java\jdk1.8.0_131"  1>nul
if not errorlevel 1 goto valid_JAVA_HOME_DIR
set JAVA_EXE=C:\SDK\Java\jdk1.8.0_131\bin\java.exe
if exist "C:\SDK\Java\jdk1.8.0_131\bin\java.exe" goto valid_JAVA_HOME
if exist "C:\SDK\Java\jdk1.8.0_131\lib\tools.jar" set TOOLS_JAR=C:\SDK\Java\jdk1.8.0_131\lib\tools.jar
if "" == "" set GROOVY_HOME=C:\hoge\groovy-2.5.0-beta-1\bin\..
if "." == "\" SET GROOVY_HOME=C:\hoge\groovy-2.5.0-beta-1\bin\.
set _SKIP=2
set CP=
if "xMain.groovy" == "x-cp" set CP=
if "xMain.groovy" == "x-classpath" set CP=
if "xMain.groovy" == "x--classpath" set CP=
if "x" == "x" goto init
set GROOVY_SCRIPT_NAME=C:\hoge!\Main.groovy
if not "Windows_NT" == "Windows_NT" goto win9xME_args
if "eval[2+2]" == "4" goto 4NT_args
set CMD_LINE_ARGS=
if "xMain.groovy" == "x" goto execute
rem horrible roll your own arg processing inspired by jruby equivalent
rem escape minus (-d), quotes (-q), star (-s).
set _ARGS="C:\hoge!\groovy-2.5.0-beta-1\bin\" groovy.ui.GroovyMain Main.groovy
if not defined _ARGS goto execute
set _ARGS="C:\hoge\groovy-d2.5.0-dbeta-d1\bin\" groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
rem Windows will try to match * with files so we escape it here
rem but it is also a meta char for env var string substitution
rem so it can't be first char here, hack just for common cases.
rem If in doubt use a space or bracket before * if using -e.
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
set _ARGS=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy
rem prequote all args for 'for' statement
set _ARGS="-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy"
set _ARG=
rem split args by spaces into first and rest
for /F "tokens=1,*" %i in ("-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q groovy.ui.GroovyMain Main.groovy") do call :get_arg "%i" "%j"
call :get_arg "-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q" "groovy.ui.GroovyMain Main.groovy"
rem remove quotes around first arg
for %i in ("-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q") do set _ARG= %~i
set _ARG= -qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q
rem set the remaining args
set _ARGS="groovy.ui.GroovyMain Main.groovy"
rem remove the leading space we'll add the first time
if "x " == "x " set _ARG=-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q
rem return
goto :EOF
goto process_arg
if "-qC:\hoge\groovy-d2.5.0-dbeta-d1\bin\-q" == "" goto execute
rem collect all parts of a quoted argument containing spaces
if not "-q" == "-q" goto :argIsComplete
if "-q" == "-q" goto :argIsComplete
if "x4" == "x2" goto skip_4
if "x3" == "x2" goto skip_3
if "x2" == "x2" goto skip_2
set _ARG=
set _SKIP=1
goto win9xME_args_loop
rem split args by spaces into first and rest
for /F "tokens=1,*" %i in ("groovy.ui.GroovyMain Main.groovy") do call :get_arg "%i" "%j"
call :get_arg "groovy.ui.GroovyMain" "Main.groovy"
rem remove quotes around first arg
for %i in ("groovy.ui.GroovyMain") do set _ARG= %~i
set _ARG= groovy.ui.GroovyMain
rem set the remaining args
set _ARGS="Main.groovy"
rem remove the leading space we'll add the first time
if "x " == "x " set _ARG=groovy.ui.GroovyMain
rem return
goto :EOF
goto process_arg
if "groovy.ui.GroovyMain" == "" goto execute
rem collect all parts of a quoted argument containing spaces
if not "gr" == "-q" goto :argIsComplete
if "x4" == "x1" goto skip_4
if "x3" == "x1" goto skip_3
if "x2" == "x1" goto skip_2
if "x1" == "x1" goto skip_1
set _ARG=
set _SKIP=0
goto win9xME_args_loop
rem split args by spaces into first and rest
for /F "tokens=1,*" %i in ("Main.groovy") do call :get_arg "%i" "%j"
call :get_arg "Main.groovy" ""
rem remove quotes around first arg
for %i in ("Main.groovy") do set _ARG= %~i
set _ARG= Main.groovy
rem set the remaining args
set _ARGS=""
rem remove the leading space we'll add the first time
if "x " == "x " set _ARG=Main.groovy
rem return
goto :EOF
goto process_arg
if "Main.groovy" == "" goto execute
rem collect all parts of a quoted argument containing spaces
if not "Ma" == "-q" goto :argIsComplete
if "x4" == "x0" goto skip_4
if "x3" == "x0" goto skip_3
if "x2" == "x0" goto skip_2
if "x1" == "x0" goto skip_1
rem now unescape -s, -q, -n, -d
rem -d must be the last to be unescaped
set _ARG=Main.groovy
set _ARG=Main.groovy
set _ARG=Main.groovy
set _ARG=Main.groovy
set CMD_LINE_ARGS= Main.groovy
set _ARG=
goto win9xME_args_loop
rem split args by spaces into first and rest
for /F "tokens=1,*" %i in ("") do call :get_arg "%i" "%j"
goto process_arg
if "" == "" goto execute
set STARTER_CLASSPATH=C:\hoge\groovy-2.5.0-beta-1\bin\..\lib\groovy-2.5.0-beta-1.jar
if exist "C:\Users\Ralph/.groovy/init.bat" call "C:\Users\Ralph/.groovy/init.bat"
if "x" == "x" goto empty_cp
set CP=.
if "x" == "x" goto after_cp
set STARTER_MAIN_CLASS=org.codehaus.groovy.tools.GroovyStarter
set STARTER_CONF=C:\hoge\groovy-2.5.0-beta-1\bin\..\conf\groovy-starter.conf
set GROOVY_OPTS="-Xmx128m"
set GROOVY_OPTS="-Xmx128m" -Dprogram.name=""
set GROOVY_OPTS="-Xmx128m" -Dprogram.name="" -Dgroovy.home="C:\hoge\groovy-2.5.0-beta-1\bin\.."
if not "C:\SDK\Java\jdk1.8.0_131\lib\tools.jar" == "" set GROOVY_OPTS="-Xmx128m" -Dprogram.name="" -Dgroovy.home="C:\hoge\groovy-2.5.0-beta-1\bin\.." -Dtools.jar="C:\SDK\Java\jdk1.8.0_131\lib\tools.jar"
set GROOVY_OPTS="-Xmx128m" -Dprogram.name="" -Dgroovy.home="C:\hoge\groovy-2.5.0-beta-1\bin\.." -Dtools.jar="C:\SDK\Java\jdk1.8.0_131\lib\tools.jar" -Dgroovy.starter.conf="C:\hoge\groovy-2.5.0-beta-1\bin\..\conf\groovy-starter.conf"
set GROOVY_OPTS="-Xmx128m" -Dprogram.name="" -Dgroovy.home="C:\hoge\groovy-2.5.0-beta-1\bin\.." -Dtools.jar="C:\SDK\Java\jdk1.8.0_131\lib\tools.jar" -Dgroovy.starter.conf="C:\hoge\groovy-2.5.0-beta-1\bin\..\conf\groovy-starter.conf" -Dscript.name="C:\hoge\Main.groovy"
if exist "C:\Users\Ralph/.groovy/postinit.bat" call "C:\Users\Ralph/.groovy/postinit.bat"
"C:\SDK\Java\jdk1.8.0_131\bin\java.exe" "-Xmx128m" -Dprogram.name="" -Dgroovy.home="C:\hoge\groovy-2.5.0-beta-1\bin\.." -Dtools.jar="C:\SDK\Java\jdk1.8.0_131\lib\tools.jar" -Dgroovy.starter.conf="C:\hoge\groovy-2.5.0-beta-1\bin\..\conf\groovy-starter.conf" -Dscript.name="C:\hoge\Main.groovy"  -classpath "C:\hoge\groovy-2.5.0-beta-1\bin\..\lib\groovy-2.5.0-beta-1.jar" org.codehaus.groovy.tools.GroovyStarter --main groovy.ui.GroovyMain --conf "C:\hoge\groovy-2.5.0-beta-1\bin\..\conf\groovy-starter.conf" --classpath "."  Main.groovy
if "Windows_NT" == "Windows_NT" endlocal
if "" == "on" pause
C:\WINDOWS\system32\cmd.exe /C exit /B 1

ここで注目するのは、156行目の、実際に java.exe を起動させている部分で、hoge! ディレクトリが全部 ! が外れて hoge ディレクトリとして呼び出してしまっています。

この原因は、Windowsのバッチファイルで使用できる遅延環境変数を Groovy の起動スクリプト内で使用しているからです。

Groovy を起動させるスクリプトの本体である、 bin/startGroovy.bat を見ると、以下のような記述があります。

1
2
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal enabledelayedexpansion

この enabledelayedexpansion がで遅延環境変数が定義されると、感嘆符に対してエスケープが必要になります。( !VAR! が遅延環境変数になるので)

基本的には、感嘆符の前に ^^ をつけてエスケープすればいいのですが、今回は、Groovy内部で不定の回数展開される変数 GROOVY_OPTS などで使用されているため、単純に GROOVY_HOME を指定したり、呼び出し時のスクリプトパスを工夫するのでは起動しません。

仕方がないので、 startGroovy.bat に手を少し加えてみます。

感嘆符で検索すると、このバッチファイル内部で遅延環境変数が使われているのは、 JAVA_HOME が未定義時に PATH から有効な Java ランタイムを検索する部分で使われています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
:check_JAVA_HOME
@rem Make sure we have a valid JAVA_HOME
if not "%JAVA_HOME%" == "" goto have_JAVA_HOME
set PATHTMP=%PATH%
:loop
for /f "delims=; tokens=1*" %%i in ("!PATHTMP!") do (
    if exist "%%i\..\bin\java.exe" (
        set "JAVA_HOME=%%i\.."
        goto found_JAVA_HOME
    )
    set PATHTMP=%%j
    goto loop
)
goto check_default_JAVA_EXE

奇しくも Groovy 便利機能の一つが今回の引き金になってしまいました。

とりあえず、遅延環境変数はこの範囲内だけで動いていれば問題ないはずなので、この範囲内だけ有効になるようにしてみます。

ファイル先頭の enabledelayedexpansion を消した上で、上記範囲を以下のように書き換えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
:check_JAVA_HOME
@rem Make sure we have a valid JAVA_HOME
if not "%JAVA_HOME%" == "" goto have_JAVA_HOME
setlocal enabledelayedexpansion
set PATHTMP=%PATH%
:loop
for /f "delims=; tokens=1*" %%i in ("!PATHTMP!") do (
    if exist "%%i\..\bin\java.exe" (
        set "JAVA_HOME=%%i\.."
        setlocal disabledelayedexpansion
        goto found_JAVA_HOME
    )
    set PATHTMP=%%j
    goto loop
)
setlocal disabledelayedexpansion
goto check_default_JAVA_EXE

この状態で run.bat を起動してみましょう。すると、別のエラーに変わっているはずです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java.io.IOException: Stream closed
        at java.io.BufferedInputStream.getInIfOpen(BufferedInputStream.java:159)
        at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
        at java.io.BufferedInputStream.read(BufferedInputStream.java:265)
        at java.io.DataInputStream.readUnsignedShort(DataInputStream.java:337)
        at java.io.DataInputStream.readUTF(DataInputStream.java:589)
        at java.io.DataInputStream.readUTF(DataInputStream.java:564)
        at org.codehaus.groovy.reflection.GeneratedMetaMethod$DgmMethodRecord.loadDgmInfo(GeneratedMetaMethod.java:177)
        at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.registerMethods(MetaClassRegistryImpl.java:189)
        at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java:96)
        at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java:74)
        at groovy.lang.GroovySystem.<clinit>(GroovySystem.java:36)
        at org.codehaus.groovy.runtime.InvokerHelper.<clinit>(InvokerHelper.java:86)
        at groovy.lang.GroovyObjectSupport.<init>(GroovyObjectSupport.java:34)
        at groovy.lang.Binding.<init>(Binding.java:35)
        at groovy.lang.GroovyShell.<init>(GroovyShell.java:74)
        at groovy.ui.GroovyMain.processOnce(GroovyMain.java:593)
        at groovy.ui.GroovyMain.run(GroovyMain.java:326)
        at groovy.ui.GroovyMain.process(GroovyMain.java:312)
        at groovy.ui.GroovyMain.processArgs(GroovyMain.java:129)
        at groovy.ui.GroovyMain.main(GroovyMain.java:109)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.codehaus.groovy.tools.GroovyStarter.rootLoader(GroovyStarter.java:107)
        at org.codehaus.groovy.tools.GroovyStarter.main(GroovyStarter.java:129)
Caught: java.lang.ExceptionInInitializerError
java.lang.ExceptionInInitializerError
Caused by: groovy.lang.GroovyRuntimeException: Unable to load module META-INF descriptor
Caused by: java.io.FileNotFoundException: C:\hoge (指定されたファイルが見つかりません。)

java.io.FileNotFoundException: C:\hogegroovy.lang.GroovyRuntimeException: Unable to load module META-INF descriptor ということで、META-INF を探しに行っているようですが、どうも検索しているファイルがおかしいです。

予想するに感嘆符が区切りとして認識されてしまっているので、Groovyのライブラリロードで死んでいるのかなーと言う感じです。

とりあえず、この内部の探索と修正は非効率的で、感嘆符がつかないようにさえすればそもそも回避できるので、ここで調査は終了とします。

結論

フォルダ名は英数半角及び特殊じゃなさそうな記号で構成しよう