LoginSignup
3
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-03-05

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

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1