カレントディレクトリ情報を保持するProcessBuilder実装

# 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
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`を受け渡してやることで、指定されたディレクトリからコマンドを実行することはできる。

それにしても、毎回以下のように実行ディレクトリを指定する必要があるのは少々面倒臭い。

```Scala
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

```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

```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

```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`が返すことになる。色々改修が必要そうな箇所はあるかもしれないが、とりあえずは目的を達成した。