scala.sys.process.Process
Scalaからコマンドラインを叩く方法としてscala.sys.process.Process
を使う方法がある。Process("pwd")
のように、実行したいコマンドを文字列として渡すことでscala.sys.process.ProcessBuilder
が生成される。この生成されたProcessBuilder
を実行することで、コマンドが実行される。
例えばコマンドライン上でecho foo && echo bar
のように表現されるものは、Process("echo foo") #&& Process("echo bar") !!
のように書けば良い。
この動作は、sbt console上でも簡単に確認することができる。直前の例を実行してみる。
scala> import scala.sys.process.Process
import scala.sys.process.Process
scala> val result = Process("echo foo") #&& Process("echo bar") !!
result: String =
"foo
bar
"
なお、!!
メソッドは生成されたProcessBuilder
を順次実行する。そしてその結果を文字列として返す。その他にも様々なメソッドがある。
個人的な問題点
しかしscala.sys.process.ProcessBuilder
を#&&
や###
どのメソッドでチェーンしていく場合、不都合があることがわかった。それは、一つ前の操作で実行したcd
コマンドの結果が、次の操作では反映されていないということだ。つまり、Process("pwd") #&& Process("cd ..") #&& Process("pwd") !!
を実行した場合、最初のpwd
の結果と、最後のpwd
の結果が同じになる。
だが、コマンドの実行が常に同じディレクトリからしか実行できないという訳ではない。Process("pwd", new File(dirPath))
のようにProcess
に実行したいディレクトリを表すjava.lang.File
を受け渡してやることで、指定されたディレクトリからコマンドを実行することはできる。
それにしても、毎回以下のように実行ディレクトリを指定する必要があるのは少々面倒臭い。
Process("echo foo", new File(dirPath)) #&& Process("echo bar", new File(dirPath))
そこでcd
をプログラム側で制御し、カレントディレクトリ情報を保持したままProcessBuilder
を構築するメソッドチェーンを繋げることができる新たなProcessBuilder
が必要になった。
解決策
以下の3つのファイルによって実現される。コードは以下の通りだ。
- process.Process.scala
- process.ProcessBuilder.scala
- process.impl.ProcessBuilder.scala
ProcessBuilder.scala
package process
trait ProcessBuilder {
/**
* カレントディレクトリの移動.
* このメソッドの後ろにチェーンするProcessBuilderはこのメソッドによる
* 実行ディレクトリの変更の影響を受ける.
* @param directoryPath ディレクトリ指定: 絶対パス or 相対パス
* @return カレントディレクトリ移動後のProcessBuilder
*/
def cd(directoryPath: String): ProcessBuilder
/**
* ProcessBuilderを逐次的に実行する
* @return 実行結果の文字列
*/
def !! : String
/**
* 失敗した場合には後続の処理は実行されない.
* @param nextProcess 現在のプロセスが成功した場合に実行するプロセス
* @return 新しいProcessBuilder
*/
def #&&(nextProcess: String): ProcessBuilder
/**
* nextProcessを現在のProcessBuilderの後ろに繋げる.
* 失敗した場合には後続の処理は実行されない.
* @param nextProcess 現在のプロセスが成功した場合に実行するプロセス
* @return 新しいProcessBuilder
*/
def #&&(nextProcess: Seq[String]): ProcessBuilder
/**
* nextProcessを現在のProcessBuilderの後ろに繋げる.
* @param nextProcess 現在のプロセスが終了した後に実行するプロセス
* @return 新しいProcessBuilder
*/
def ###(nextProcess: String): ProcessBuilder
/**
* nextProcessを現在のProcessBuilderの後ろに繋げる.
* @param nextProcess 現在のプロセスが終了した後に実行するプロセス
* @return 新しいProcessBuilder
*/
def ###(nextProcess: Seq[String]): ProcessBuilder
}
impl.ProcessBuiler.scala
package process.impl
import java.io.File
import process
private[process] final class ProcessBuilder(
builder: scala.sys.process.ProcessBuilder,
currentDirectory: File)
extends process.ProcessBuilder {
import ProcessBuilder._
@scala.annotation.tailrec
def parseRelativePath(relativePath: String, currentDirectory: File): File =
relativePath match {
case Slush(rest) => parseRelativePath(rest, currentDirectory)
case Current(rest) => parseRelativePath(rest, currentDirectory)
case Parent(rest) => parseRelativePath(rest, currentDirectory.getParentFile)
case Child(child, null) => new File(s"${currentDirectory.getAbsolutePath}/$child")
case Child(child, rest) => parseRelativePath(rest, new File(s"${currentDirectory.getAbsolutePath}/$child"))
case _ => throw new UnknownError() // Unreachable
}
override def cd(directoryPath: String): ProcessBuilder =
directoryPath match {
case absolutePath if absolutePath startsWith "/" =>
new ProcessBuilder(builder, new File(directoryPath))
case relativePath =>
new ProcessBuilder(builder, parseRelativePath(relativePath, currentDirectory))
}
override def #&&(nextProcess: String): ProcessBuilder =
new ProcessBuilder(
builder #&& scala.sys.process.Process(nextProcess, currentDirectory), currentDirectory)
override def #&&(nextProcess: Seq[String]): ProcessBuilder =
new ProcessBuilder(
builder #&& scala.sys.process.Process(nextProcess, currentDirectory), currentDirectory)
override def !! : String = builder.!!
override def ###(nextProcess: String): process.ProcessBuilder =
new ProcessBuilder(
builder ### scala.sys.process.Process(nextProcess, currentDirectory), currentDirectory)
override def ###(nextProcess: Seq[String]): process.ProcessBuilder =
new ProcessBuilder(
builder ### scala.sys.process.Process(nextProcess, currentDirectory), currentDirectory)
}
object ProcessBuilder {
private val Current = """\.([^\.].*)?""".r
private val Parent = """\.\.([^\.].*)?""".r
private val Child = """([^\./]+)([\./].*)?""".r
private val Slush = """/(.*)""".r
}
Process.scala
package process
import java.io.File
object Process {
private val currentDir = new File(".").getAbsoluteFile.getParentFile
def apply(processString: String): ProcessBuilder =
new impl.ProcessBuilder(scala.sys.process.Process(processString, currentDir), currentDir)
def apply(processString: String, dirPath: String): ProcessBuilder =
new impl.ProcessBuilder(scala.sys.process.Process(processString, new File(dirPath)), new File(dirPath))
def apply(processSeq: Seq[String]): ProcessBuilder =
new impl.ProcessBuilder(scala.sys.process.Process(processSeq, currentDir), currentDir)
def apply(processSeq: Seq[String], dirPath: String): ProcessBuilder =
new impl.ProcessBuilder(scala.sys.process.Process(processSeq, new File(dirPath)), new File(dirPath))
def empty: ProcessBuilder =
new impl.ProcessBuilder(scala.sys.process.Process("echo"), currentDir)
}
まとめ
端的にいえば、scala.sys.process.ProcessBuilder
に対するAdapterパターンだ。process.ProcessBuilder
トレイトでは、最低限のメソッドを定義している。Process("pwd"). cd (".."). #&& ("pwd"). !!
のように実行すれば、初回のpwd
で表示された親ディレクトリの結果を最後のpwd
が返すことになる。色々改修が必要そうな箇所はあるかもしれないが、とりあえずは目的を達成した。