S-JIS[2011-07-28/2011-07-29] 変更履歴

JavaでCygwin呼び出し

WindowsのJavaからCygwin経由でシェルを実行する方法を考えてみる。


概要

UNIX環境では、ProcessBuilderクラスを使ってJavaからシェルを実行することが出来る。

	List<String> list = new ArrayList<String>();
	list.add("/home/hishidama/run.sh");

	ProcessBuilder pb = new ProcessBuilder(list);
	Process p = pb.start();

しかし当然ながら、Windows環境ではシェルを実行することは出来ない。

なので、JavaはWindows上でコンパイル・単体テストしてclassファイルをUNIXに持っていって実行するということが出来るが、
シェルを実行するようなプログラムだと、Windows上で実行(単体テスト)することは出来ない。


そこでCygwinを使ってWindows環境でもシェルを実行する方法を考えた。
直接呼び出すのはbash.exeにする。

	List<String> list = new ArrayList<String>();
	String os = System.getProperty("os.name");
	if (os != null && os.startsWith("Windows")) {
		list.add(CYGWIN_HOME + "/bin/bash.exe");
		list.add("--login");
		// list.add("-x");
	}
	list.add("/home/hishidama/run.sh");

	ProcessBuilder pb = new ProcessBuilder(list);
	Process p = pb.start();

(これを応用して、Antからもシェルをキックできる→AntからCygwinのコマンドを実行する例

しかしこの方法では、自分でプログラムを作るときはいいが、自分でいじれないプログラムでシェルを起動するようになっているときは困る。
(それに、そもそも環境によって動くロジックが異なるので、厳密には単体テストにはならない)


ところで、Windowsでは、実行モジュール(exeやbat)は拡張子を省略しても実行できる
これを使って何とか出来ないだろうか?と思って試してみた。


.shの関連付けを変えてみる

Windowsでは、拡張子によってどのアプリケーションを実行するかを設定できる。→拡張子の関連付け
関連付けを行うと、(コマンドプロンプトからは)拡張子を省略して実行することが出来る。

これを使って、「.sh」のときはbash.exeを実行するような関連付けにしてみる。

> assoc .sh=shfile
> ftype shfile=C:\cygwin\bin\bash.exe --login %1 %*

こうすれば、コマンドプロンプトからrun.shを叩くとちゃんと実行できる。

しかし残念なことに、ProcessBuilderからは実行できない。

	List<String> list = new ArrayList<String>();
	list.add("/temp/run.sh");	//Windowsでは「C:\temp\run.sh」を指している

	ProcessBuilder pb = new ProcessBuilder(list);
	Process p = pb.start();

なぜかと言うと、これはDOSの内部コマンド(echoとか)を実行するのと同じなのだろう。(内部コマンドをProcessBuilderで直接実行することは出来ない
内部コマンドの実行はcmd.exeが解釈して行うので、 拡張子連動したシェルもcmd経由なら実行できる。

	List<String> list = new ArrayList<String>();
	list.add("cmd");
	list.add("/c");
	list.add("/temp/run.sh");	//Windowsでは「C:\temp\run.sh」を指している

	ProcessBuilder pb = new ProcessBuilder(list);
	Process p = pb.start();

でもそれじゃ意味が無ーい!(苦笑)


exeにしてみる

Windows環境ではProcessBuilderはexeファイルを実行するものであり、その場合は拡張子「.exe」を省略できる。
であれば、「.sh.exe」というexeファイルなら実行できそうではないか。

CygwinにCコンパイラーを入れて、exeファイルを作ってみる。

C:\temp\run.c(/cygdrive/c/temp/run.c):

#include <stdio.h>

int main(int argc, char *argv[]) {
	printf("run.c!! %d\n", argc);
	return 0;
}

Cygwin上でコンパイル・実行:

$ gcc run.c -o run.sh.exe

$ ./run.sh
run.c!! 1

まずは良し。

コマンドプロンプトから実行:

C:\temp> run.sh

「cygwin1.dllが見つからなかったため、このアプリケーションを開始できませんでした」というエラーが出た^^;
環境変数PATHに「C:\cygwin\bin」(cygwin1.dllのある場所)が入ってないと、こうなるんだよね。

C:\temp> PATH=%PATH%;C:\cygwin\bin

C:\temp> run.sh
run.c!! 1

今度はOK!

Java(ProcessBuilder)から実行:

実行する際に、環境変数PATHに「C:\cygwin\bin」が入った状態にしておく。

	List<String> list = new ArrayList<String>();
	list.add("/temp/run.sh");	//Windowsでは「C:\temp\run.sh」を指している

	ProcessBuilder pb = new ProcessBuilder(list);
	Process p = pb.start();

実行結果は省略するが、動いたよ!(笑)


そういうわけで、自分と同名のshを実行するexeを作ってみた。[/2011-07-29]

shbridge.c

#include <stdio.h>
#include <stdlib.h>

static char* cut(char *p) {
	if (strncmp(p, "/cygdrive/", 10) == 0) {
		return p + 11;
	}
	return p;
}

int main(int argc, char *argv[]) {
	int i;

	int len = 0;
	for (i = 0; i < argc; i++) {
		len += strlen(argv[i]) + 1;
	}

	char buf[len];
	strcpy(buf, cut(argv[0]));
	for (i = 1; i < argc; i++) {
		strcat(buf, " ");
		strcat(buf, argv[i]);
	}

	//printf("debug->%s\n", buf);
	int r = system(buf);
	return r >> 8;
}

Cygwinで作られたexeの場合、JavaのProcessBuilderからフルパスで「/home/hishidama/run.sh」の様に呼ぶと、「/cygdrive/c/」が付けられて
argv[0]には「/cygdrive/c/home/hishidama/run.sh」が入ってくる。
そこで、その部分を取り除いて呼び出すようにした。
これでCygwin上の「/home/hishidama/run.sh」が呼ばれる。

$ gcc shbridge.c -o shbridge.exe

$ mkdir -p /cygdrive/c/home/hishidama/
$ cp -p shbridge.exe /cygdrive/c/home/hishidama/run.sh.exe

$ cat /home/hishidama/run.sh
echo home-run.sh!! $@
exit 126

使うときは、exeファイル(shbridge.exe)を“呼ばれるシェル名”+「.exe」というファイル名に変えて(コピーして)おく。
呼ばれるexeはWindowsでのパスの場所「C:\home\hishidama(/cygdrive/c/home/hishidama)」に置き、
そこから実際に実行されるシェルはCygwin上のパス「/home/hishidama(C:\cygwin\home\hishidama)」に置く。

ProcessBuilderで呼び出すコード:

	List<String> list = new ArrayList<String>();
	list.add("/home/hishidama/run.sh");
	list.add("java");
	list.add("a*");

	ProcessBuilder pb = new ProcessBuilder(list);
	pb.redirectErrorStream(true);
	Process p = pb.start();
	InputStream is = p.getInputStream();

	printInputStream(is);

	int r = p.waitFor();
	System.out.println("戻り値:" + r);

※実行する際には、環境変数PATHに「C:\cygwin\bin」を入れておくのを忘れずに!
 「戻り値:-1073741515」と表示されたら、これが設定されていない可能性あり。

実行結果:

home-run.sh!! java a*
戻り値:126

やったね!

なお、このshbridge.cは、見ての通りマルチバイト文字には対応してないし、system関数をそのまま呼んでいるので空白文字入りの引数やワイルドカードには対応していない。


exec関数で呼び出す方法でも出来る。[2011-07-29]
こちらの方がシンプルか?
(exec関数は実行ファイル(Windowsのexeファイル)しか呼べないが、system関数はシェルも呼べるというが一番大きな違い)

#include <stdio.h>
#include <unistd.h>

static char* cut(char *p) {
	if (strncmp(p, "/cygdrive/", 10) == 0) {
		return p + 11;
	}
	return p;
}

int main(int argc, char *argv[]) {
	int i;

	char *args[argc + 2];
	args[0] = "bash.exe";
	args[1] = cut(argv[0]);
	for (i = 1; i < argc; i++) {
		args[i+1] = argv[i];
	}
	args[argc+1] = NULL;

	return execvp(args[0], args);
}

batにしてみる

拡張子をexeにして「.sh.exe」を呼び出すのと同様に、拡張子をbatにして「.sh.bat」というファイルが呼べるか
を試してみたが、こちらは出来なかった。[2011-07-29]
batファイルの方がコンパイル要らないから手軽に作れるし、そもそもUNIXのシェルスクリプトに相当するのがDOSのバッチファイルだから、バッチファイルを呼ぶというのもアリなんだけどなー。残念。


Java目次へ戻る / Cygwinへ戻る / 技術メモへ戻る
メールの送信先:ひしだま