2010年1月19日 星期二

Here Document與Java程式的搭配使用

這幾天一直百思不得其解,從前利用Here Document來傳值給C++/Fortran程式都能夠輕易完成;昨天寫了一個小小的轉換格式的程式想搭配shell script完成任務卻出現了NoSuchElementException 的尷尬問題,怎麼嘗試都不成功。

所謂Here Document,意思就是在子process裡傳入參數。在父process還在運行的時候,預先把參數值寫好,等到運行到子process的時候,不必等待使用者提示即可傳值。舉個例子:

假設someprog是一個C++/Fortran程式,過程中會提示,輸入兩個字串兩個數值:

#!/bin/sh
/usr/local/bin/someprog << END
vespa.env.out
vespa.ent.OUT
23.5
2.0
END

這種在script裡就先把要傳入的參數先寫好的方法,便是常被使用的Here Document。然而,今天我也寫了一個落落長的java互動式程式,卻拋出了NoSuchElementException 的例外。為了檢查到底是哪裡出問題,我寫了一個更簡單的程式來代替:

假設這個程式稱為TestString:

import java.util.*;

public class TestString{
  public static void main(String[] args){
    System.out.println("1st enter:");
    String tmpstr = new Scanner(System.in).nextLine();

    System.out.println("1st enter is "+tmpstr+". Now enter 2nd :");
    tmpstr = new Scanner(System.in).nextLine();

    System.out.println("2nd enter is "+tmpstr);
 }
}

編譯他並包成jar檔:
jar -cmvf menifest.mf TestString.class
當然,你的menifest.mf在預設套件的情況下可能會長這樣:
Main-Class: TestString

由於java程式是byte-code而非native-code,因此需要jvm去驗證並啟動他;想要模擬C++程式直接呼叫binary這樣的方法,只需要一個小小的script來解決:首先建立一個跟java程式同檔名的文字檔,以此例即為TestString,其內容如下:

#!/bin/bash
#Program TestString
[ -e `which java` ] && java -jar /home/maxsolar/bin/TestString.jar

exit 0;

當然別忘記把這個script加上可執行屬性,並放在你的PATH範圍裡。
執行以下的script來試試看!

#!/bin/bash
TestString << END
string1
string2
END

結果卻出現了下列錯誤訊息:
1st enter:
1st enter is string1. Now enter 2nd :
Exception in thread "main" java.util.NoSuchElementException: No line found
    at java.util.Scanner.nextLine(Scanner.java:1516)
    at TestString.main(TestString.java: 9)

對還不太能掌握java的我,實在不清楚究竟是為了麼值會傳不進去。困擾我一整天之後,決定到台灣最大的JavaWorld@TW論壇去爬文,卻沒有跟我的情況有類似的問題。後來在版上發問,沒想到很快的就有高手來解決我的問題:

Scanner 在某些操作上會有 buffering 的行為,如果要透過 Scanner 來處理 standard input 裡的數據,應只使用一個 Scanner instance。

import java.util.*;
public class TestString {

    public static void main(String[] args) {
        System.out.println("1st enter:");
        final Scanner scanner = new Scanner(System.in);
        String tmpstr = scanner.nextLine();

        System.out.println("1st enter is " + tmpstr + ". Now enter 2nd :");
        tmpstr = scanner.nextLine();

        System.out.println("2nd enter is " + tmpstr);
    }
}

如此一來,我使用的Scanner物件將會是唯一。先前我所使用的是anonymous的方法產生物件並直接傳到tmpstr這個字串物件接收,但是根據Dunkan前輩的建議,要使用一個唯一的Scanner物件來接收我的標準輸入,才能擺脫被buffered在記憶體的問題。

非常感謝Dunkan前輩精確且迅速的解答,因此把這篇作成筆記,好讓自己跟需要的Java與linux同好可以有機會參考。

參考連結:
[請益]shell script呼叫java程式並傳入值的問題