1. 程式人生 > 其它 >Groovy系列(5)- Groovy IO操作

Groovy系列(5)- Groovy IO操作

IO操作

Groovy為I/O操作提供了許多幫助方法,雖然你可以在Groovy中用標準Java程式碼來實現I/O操作,不過Groovy提供了大量的方便的方式來操作File、Stream、Reader等等。

讀取檔案

讀取文字檔案並列印每一行文字

new File(baseDir, 'haiku.txt').eachLine{ line ->
    println line
}

eachLine方法是Groovy為File類自動新增的方法,同時提供多個變體方法,比如你想知道讀取的行數,你可以使用它的變體方法,如下

new File(baseDir, 'haiku.txt').eachLine{ line, nb ->
    println "Line $nb: $line"
}

如果出於某種原因eachLine丟擲異常,該方法能夠確保資源正確地關閉,這適用於所有Groovy新增的I/O資源方法

例如在某些情況下,你更喜歡使用Reader訪問I/O資源,但你仍然受益於Groovy的自動資源管理。在下一個示例中,即使發生了異常,Reader也將會被關閉。

def count = 0, MAXSIZE = 3
new File(baseDir,"haiku.txt").withReader { reader ->
    while (reader.readLine()) {
        if (++count > MAXSIZE) {
            throw new RuntimeException('Haiku should only have 3 verses')
        }
    }
}

如果你需要收集文字檔案的每一行到一個列表中,你可以這樣做:

def list = new File(baseDir, 'haiku.txt').collect {it}

或者你甚至可以用as操作符來講檔案內容轉為String陣列

def array = new File(baseDir, 'haiku.txt') as String[]

多少次你不得不獲得一個檔案的內容到一個byte[],它需要多少程式碼?Groovy使得這件事非常的容易。

byte[] contents = file.bytes

處理I/O並不侷限於處理檔案。事實上,很多的操作依賴於輸入/輸出流,這就是為什麼Groovy新增大量的支援方法,正如你所看到的文件。

例如,你可以很容易從一個檔案獲得一個InputStream

def is = new File(baseDir,'haiku.txt').newInputStream()
// do something ...
is.close()

但是你可以看到,這種方式需要你手動關閉流。會為你留意到,在Groovy中使用withInputStream通常是更好的方式。

new File(baseDir,'haiku.txt').withInputStream { stream ->
    // do something ...
}

寫檔案

當然,一些時候你並不想讀取檔案,而是要寫檔案。其中一個方式就是使用Writer:

new File(baseDir,'haiku.txt').withWriter('utf-8') { writer ->
    writer.writeLine 'Into the ancient pond'
    writer.writeLine 'A frog jumps'
    writer.writeLine 'Water’s sound!'
}

但是對於這樣一個簡單的示例中,使用<<操作符就足夠了:

new File(baseDir,'haiku.txt') << '''Into the ancient pond
A frog jumps
Water’s sound!'''

當然我們並不總是處理文字內容,所以您可以使用Writer或者直接寫入位元組,例如:

file.bytes = [66,22,11]

當然你也可以直接處理輸出流。例如,下面就是如何建立一個輸出流寫入一個檔案:

def os = new File(baseDir,'data.bin').newOutputStream()
// do something ...
os.close()

但是你可以看到它需要你手動關閉輸出流。會為你留意到,在Groovy中使用withOutputStream通常是更好的方式。

new File(baseDir,'data.bin').withOutputStream { stream ->
    // do something ...
}

遍歷檔案樹

在指令碼中,在檔案樹種查詢一些特定檔案並處理這些檔案是很常見的事情。Groovy提供了多種方法來做到這一點。例如你可以為一個資料夾中的每個檔案直行一些操作:

//在目錄的每一個檔案直行閉包程式碼
dir.eachFile { file ->                      
    println file.name
}

//在目錄中符合匹配模式的檔案直行閉包程式碼
dir.eachFileMatch(~/.*\.txt/) { file ->     
    println file.name
}

通常你需要處理一個更深的目錄結構的檔案,這種情況下你可以使用eachFileRecurse

//在指定目錄遞迴查詢,並在每一個檔案和目錄直行閉包程式碼
dir.eachFileRecurse { file ->                      
    println file.name
}

//在指定目錄遞迴查詢,並在每一個檔案直行閉包程式碼
dir.eachFileRecurse(FileType.FILES) { file ->      
    println file.name
}

對於更復雜的遍歷技術可以使用traverse方法,它你需要設定一個特殊的標誌指示這個遍歷要做什麼。

dir.traverse { file ->
    if (file.directory && file.name=='bin') {
            //如果當前file是一個目錄或者它的名字是 bin ,則停止遍歷
        FileVisitResult.TERMINATE                   
    } else {
            //列印檔名並繼續遍歷
        println file.name
        FileVisitResult.CONTINUE                    
    }

}

資料和物件

在Java中使用java.io.DataOutputStreamjava.io.DataInputStream進行序列化和反序列化並不少見,Groovy將使它更容易處理。例如,您可以把資料序列化到一個檔案中並反序列化它使用以下程式碼:

boolean b = true
String message = 'Hello from Groovy'
// Serialize data into a file
file.withDataOutputStream { out ->
    out.writeBoolean(b)
    out.writeUTF(message)
}
// ...
// Then read it back
file.withDataInputStream { input ->
    assert input.readBoolean() == b
    assert input.readUTF() == message
}

類似的,如果您想要序列化的資料實現了Serializable介面,你可以將它作為一個Object輸出流處理,例如:

Person p = new Person(name:'Bob', age:76)
// Serialize data into a file
file.withObjectOutputStream { out ->
    out.writeObject(p)
}
// ...
// Then read it back
file.withObjectInputStream { input ->
    def p2 = input.readObject()
    assert p2.name == p.name
    assert p2.age == p.age
}

執行外部程式

前一節中描述的是使用Groovy處理檔案多麼容易。然而在系統管理等領域或devops通常需要與外部processes溝通。Groovy提供了一個簡單的方法來執行命令列processes。僅僅需要些命令字串再呼叫execute()方法即可。如。在 *nix機(安裝了合適的 *nix命令工具的windows機),你這樣執行:

//在外部process執行`ls`命令
def process = "ls -l".execute()
//讀取文字並列印
println "Found text ${process.text}"

execute()方法返回一個java.lang.Process物件,並允許處理它的in/out/err流和退出值,從Process檢查等。
如,這是和上面一樣的命令

//執行`ls`命令
def process = "ls -l".execute()             
//遍歷命令程序的輸入流
process.in.eachLine { line ->               
      //列印每一行輸入流
    println line                            
}

值得注意的是,in是一個輸入流,對應於命令的標準輸出。 out指的是可以向Process傳送資料的流(標準輸入)。
記住許多命令是shell內建函式,需要特殊處理。所以,如果你想要在一個Windows機器上列出一個目錄下的所有檔案:

def process = "dir".execute()
println "${process.text}"

你會收到一個IOException說無法執行程式“dir”:CreateProcess error = 2,系統找不到指定的檔案。這是因為dir內建在Windows shell(cmd.exe)中,不能作為簡單的可執行檔案執行。 相反,你需要寫:

def process = "cmd /c dir".execute()
println "${process.text}"

此外,由於此功能當前使用java.lang.Process底層,必須考慮該類的缺陷。尤其是,這個類的javadoc說:

由於一些本地平臺僅為標準輸入和輸出流提供有限的緩衝區大小,因此無法及時寫入輸入流或讀取子過程的輸出流可能導致子過程阻塞甚至死鎖

正因為如此,Groovy提供了一些額外的幫助方法,使得流處理更容易。
這裡是如何從你的Process中gobble(未翻譯)所有的輸出(包括錯誤流輸出):

def p = "rm -f foo.tmp".execute([], tmpDir)
p.consumeProcessOutput()
p.waitFor()

還有consumeProcessOutput的變體,使用StringBufferInputStreamOutputStream等...有關完整的列表,請閱讀GDK API for java.lang.Process
此外,這些是一個pipeTo命令(對映到|以允許過載),它使一個程序的輸出流被傳送到另一個程序的輸入流。
這裡有一些使用的例子:

proc1 = 'ls'.execute()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc1 | proc2 | proc3 | proc4
proc4.waitFor()
if (proc4.exitValue()) {
    println proc4.err.text
} else {
    println proc4.text
}

消耗錯誤

def sout = new StringBuilder()
def serr = new StringBuilder()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc4.consumeProcessOutput(sout, serr)
proc2 | proc3 | proc4
[proc2, proc3].each { it.consumeProcessErrorStream(serr) }
proc2.withWriter { writer ->
    writer << 'testfile.groovy'
}
proc4.waitForOrKill(1000)
println "Standard output: $sout"
println "Standard error: $serr"