├── project └── plugins.sbt ├── src ├── main │ └── scala │ │ └── at │ │ └── loveoneanother │ │ └── schale │ │ ├── Main.scala │ │ ├── Command.scala │ │ ├── ProcControl.scala │ │ ├── package.scala │ │ ├── Shell.scala │ │ ├── Env.scala │ │ └── Proc.scala └── test │ └── scala │ └── at │ └── loveoneanother │ └── schale │ └── ProcTest.scala ├── .gitignore ├── .project ├── .classpath ├── LICENSE └── README.md /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.9.2") 2 | -------------------------------------------------------------------------------- /src/main/scala/at/loveoneanother/schale/Main.scala: -------------------------------------------------------------------------------- 1 | package at.loveoneanother.schale 2 | 3 | object Main { 4 | def main(args: Array[String]): Unit = {} 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | 12 | # Scala-IDE specific 13 | .scala_dependencies 14 | /bin 15 | .cache 16 | -------------------------------------------------------------------------------- /src/main/scala/at/loveoneanother/schale/Command.scala: -------------------------------------------------------------------------------- 1 | package at.loveoneanother.schale 2 | 3 | /** 4 | * Run a single program in default current working directory and default environment. 5 | */ 6 | object Command { 7 | def apply(args: String*)(implicit env: Env): Proc = new Proc(args: _*)(env) 8 | } -------------------------------------------------------------------------------- /src/main/scala/at/loveoneanother/schale/ProcControl.scala: -------------------------------------------------------------------------------- 1 | package at.loveoneanother.schale 2 | 3 | abstract class ProcControl 4 | // Output control 5 | case object ProcStdoutReadLine 6 | case object ProcStdoutReadChar 7 | case object ProcStderrReadLine 8 | case object ProcStderrReadChar 9 | // Input control 10 | case object ProcStdinFlush 11 | case object ProcStdinClose -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | schale 4 | 5 | 6 | 7 | 8 | 9 | org.scala-ide.sdt.core.scalabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.scala-ide.sdt.core.scalanature 16 | org.eclipse.jdt.core.javanature 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/scala/at/loveoneanother/schale/package.scala: -------------------------------------------------------------------------------- 1 | package at.loveoneanother 2 | 3 | import com.typesafe.config.ConfigFactory 4 | 5 | import akka.actor.ActorSystem 6 | import at.loveoneanother.schale.Env 7 | 8 | package object schale { 9 | implicit val defaultEnv = new Env(Map(), System getProperty "user.dir") 10 | 11 | /** The actor system is responsible for monitoring process interaction actors. */ 12 | implicit lazy val actorSystem: ActorSystem = ActorSystem("schale", ConfigFactory.parseString("akka.daemonic=on")) 13 | } -------------------------------------------------------------------------------- /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/scala/at/loveoneanother/schale/Shell.scala: -------------------------------------------------------------------------------- 1 | package at.loveoneanother.schale 2 | 3 | /** 4 | * Call operating system's script interpreter to run a script. 5 | */ 6 | object Shell { 7 | 8 | /** 9 | * Run a script using operating system's script interpreter. Currently only support *nix operating systems. 10 | */ 11 | def apply(script: String, interpreter: String = "/bin/sh")(implicit env: Env): Proc = 12 | System.getProperty("os.name").toLowerCase match { 13 | case os if os.contains("nux") || os.contains("sun") || os.contains("mac") => new Proc(interpreter, "-c", script)(env) 14 | case _ => throw new UnsupportedOperationException("Interpret does not work in your OS") 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/scala/at/loveoneanother/schale/Env.scala: -------------------------------------------------------------------------------- 1 | package at.loveoneanother.schale 2 | 3 | import java.io.File 4 | 5 | /** 6 | * Extra (or override) environment variables for running script or command. 7 | */ 8 | class Env(var vars: Map[String, String] = Map(), var pwd: String = System getProperty "user.dir") { 9 | implicit var self = this 10 | 11 | override def toString() = pwd + (vars toString) 12 | 13 | /** 14 | * Change current working directory in this environment. 15 | */ 16 | def cd(newPwd: String)(fun: => Unit) { 17 | val oldPwd = pwd 18 | pwd = newPwd 19 | try { 20 | fun 21 | } finally { 22 | pwd = oldPwd 23 | } 24 | } 25 | 26 | /** 27 | * Give more environment variables to this environment. 28 | */ 29 | def env(extra: Map[String, String])(fun: => Unit) { 30 | val oldVars = vars 31 | vars ++= extra 32 | try { 33 | fun 34 | } finally { 35 | vars = oldVars 36 | } 37 | } 38 | 39 | def applyTo(pb: ProcessBuilder) { 40 | val env = pb.environment() 41 | vars foreach { kv => env.put(kv._1, kv._2) } 42 | pb.directory(new File(pwd)) 43 | } 44 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Howard Guo 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /src/main/scala/at/loveoneanother/schale/Proc.scala: -------------------------------------------------------------------------------- 1 | package at.loveoneanother.schale 2 | 3 | import java.io.BufferedReader 4 | import java.io.BufferedWriter 5 | import java.io.InputStreamReader 6 | import java.io.OutputStreamWriter 7 | 8 | import scala.util.control.Breaks.break 9 | import scala.util.control.Breaks.breakable 10 | 11 | import akka.actor.ActorDSL.Act 12 | import akka.actor.ActorDSL.actor 13 | import akka.actor.ActorRef 14 | import akka.actor.actorRef2Scala 15 | /** 16 | * An operating system process. 17 | */ 18 | class Proc(args: String*)(env: Env) extends Traversable[String] { 19 | protected val pb = new ProcessBuilder(args: _*) 20 | protected var proc: Process = null 21 | protected var inputWriter: BufferedWriter = null 22 | protected var outputReader: BufferedReader = null 23 | protected var errorReader: BufferedReader = null 24 | 25 | /** 26 | * Start process and traverse lines in standard output. 27 | */ 28 | def stdout = new Traversable[String] { 29 | def foreach[U](fun: String => U) { 30 | startProc() 31 | collectOutput(fun, outputReader) 32 | } 33 | } 34 | 35 | /** 36 | * Start process and traverse lines in standard error. 37 | */ 38 | def stderr = new Traversable[String] { 39 | def foreach[U](fun: String => U) { 40 | startProc() 41 | collectOutput(fun, errorReader) 42 | } 43 | } 44 | 45 | /** 46 | * Start process and traverse lines in both standard output and error. 47 | */ 48 | def foreach[U](fun: String => U) { 49 | pb.redirectErrorStream(true) 50 | startProc() 51 | collectOutput(fun, outputReader) 52 | } 53 | 54 | /** 55 | * Start process and return all output in standard output and error. 56 | */ 57 | override def toString = { 58 | val output = (collect { case s: String => s }).mkString(String format "%n") 59 | waitFor() 60 | output 61 | } 62 | 63 | /** 64 | * Feed data to standard input. 65 | */ 66 | def input(lines: String*) = { 67 | startProc() 68 | try { 69 | for (s <- lines) { 70 | inputWriter.write(s) 71 | } 72 | } finally { 73 | inputWriter.close() 74 | } 75 | this 76 | } 77 | 78 | /** 79 | * Wait for process to finish and return its exit code. 80 | * If process has not been started, it will be started and then waited. 81 | */ 82 | def waitFor(): Int = { 83 | if (proc == null) 84 | startProc() 85 | try { 86 | if (inputWriter != null) 87 | inputWriter.close() 88 | // there is no need to close output readers 89 | } finally { 90 | } 91 | proc.waitFor() 92 | } 93 | 94 | /** 95 | * Start this process in background (does not block main thread). 96 | */ 97 | def bg() = this.startProc() 98 | 99 | /** 100 | * Destroy the process and return its exit value. 101 | * If process has not been started, an IllegalStateException is thrown. 102 | */ 103 | def destroy(): Int = proc match { 104 | case null => throw new IllegalStateException("Process has not started") 105 | case _ => proc.destroy(); waitFor() 106 | } 107 | 108 | /** 109 | * Start this process in background and commence interactive IO with it. 110 | */ 111 | def interact(fun: ActorRef => Unit) = { 112 | startProc() 113 | fun(actor(new Act { 114 | become { 115 | // Output control 116 | case ProcStdoutReadLine => 117 | sender ! outputReader.readLine() 118 | case ProcStdoutReadChar => 119 | sender ! outputReader.read() 120 | case ProcStderrReadLine => 121 | sender ! errorReader.readLine() 122 | case ProcStderrReadChar => 123 | sender ! errorReader.read() 124 | // Input control 125 | case s: String => 126 | inputWriter.write(s) 127 | case c: Char => 128 | inputWriter.write(c.toInt) 129 | case ProcStdinFlush => 130 | inputWriter.flush() 131 | case ProcStdinClose => 132 | inputWriter.close(); inputWriter = null 133 | } 134 | })) 135 | this 136 | } 137 | 138 | /** 139 | * Start the process. 140 | */ 141 | protected def startProc() { 142 | if (proc == null) { 143 | env.applyTo(pb) 144 | proc = pb.start() 145 | inputWriter = new BufferedWriter(new OutputStreamWriter(proc getOutputStream)) 146 | outputReader = new BufferedReader(new InputStreamReader(proc getInputStream)) 147 | errorReader = new BufferedReader(new InputStreamReader(proc getErrorStream)) 148 | } 149 | } 150 | 151 | /** 152 | * Collect process output from the reader, and feel them to the function. 153 | */ 154 | protected def collectOutput[U](fun: String => U, reader: BufferedReader) { 155 | try { 156 | breakable { 157 | while (true) { 158 | val line = reader.readLine() 159 | if (line == null) 160 | break 161 | fun(line) 162 | } 163 | } 164 | } finally { 165 | reader close 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /src/test/scala/at/loveoneanother/schale/ProcTest.scala: -------------------------------------------------------------------------------- 1 | package test.at.loveoneanother.schale 2 | 3 | import java.io.IOException 4 | 5 | import scala.concurrent.Await 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import scala.concurrent.duration.DurationInt 8 | import scala.util.Failure 9 | import scala.util.Success 10 | 11 | import org.scalatest.FunSuite 12 | 13 | import akka.actor.actorRef2Scala 14 | import akka.pattern.ask 15 | import akka.util.Timeout 16 | import at.loveoneanother.schale.Command 17 | import at.loveoneanother.schale.Env 18 | import at.loveoneanother.schale.ProcStderrReadChar 19 | import at.loveoneanother.schale.ProcStdinClose 20 | import at.loveoneanother.schale.ProcStdinFlush 21 | import at.loveoneanother.schale.ProcStdoutReadLine 22 | import at.loveoneanother.schale.Shell 23 | 24 | class ProcTest extends FunSuite { 25 | test("run process and use exit status") { 26 | expectResult(0) { Command("echo", "a").waitFor() } 27 | } 28 | 29 | test("run process without IO") { 30 | Command("echo", "a").waitFor() 31 | intercept[IOException] { 32 | Command("does not exist").waitFor() 33 | } 34 | } 35 | 36 | test("single command and collect stdout/stderr") { 37 | expectResult("a") { Command("echo", "a").toString() } 38 | } 39 | 40 | test("consume stdout") { 41 | for (line <- Command("echo", "a").stdout) 42 | expectResult("a") { line } 43 | } 44 | 45 | test("consume stderr") { 46 | for (line <- Command("echo", "a").stderr) 47 | expectResult("") { line } 48 | } 49 | 50 | test("consume stdout and stderr") { 51 | for (line <- Command("echo", "a")) 52 | expectResult("a") { line } 53 | } 54 | 55 | test("feed to stdin") { 56 | expectResult(String.format("a%nb")) { 57 | (Command("cat").input(String.format("a%n"), String.format("b%n"))).toString() 58 | } 59 | } 60 | 61 | test("feed to stdin and consume stdout/stderr") { 62 | for (line <- Command("cat").input("a")) 63 | expectResult("a") { line } 64 | } 65 | 66 | test("interpret and collect stdout/stderr") { 67 | expectResult(String.format("a")) { 68 | Shell("echo a").toString() 69 | } 70 | } 71 | 72 | test("interpret using other interpreter") { 73 | expectResult(String.format("a")) { 74 | Shell("echo a", "/bin/ksh").toString() 75 | } 76 | } 77 | 78 | test("interpret and feed to stdin, then consume stdout/stderr") { 79 | for (line <- Shell("cat").input("a")) { 80 | expectResult("a") { line } 81 | } 82 | } 83 | 84 | test("interpret and use exit status") { 85 | val interpreter = Shell("cat").input("a") 86 | for (line <- interpreter) { 87 | expectResult("a") { line } 88 | } 89 | expectResult(0) { interpreter.waitFor() } 90 | } 91 | 92 | test("destroy process") { 93 | val proc = Shell("sleep 100") 94 | intercept[IllegalStateException] { 95 | proc.destroy() 96 | } 97 | proc.bg() 98 | expectResult(143) { proc.destroy() } 99 | } 100 | 101 | test("run in specified cwd") { 102 | new Env(pwd = "/") { 103 | expectResult("/") { Command("pwd").toString } 104 | expectResult("/") { Shell("pwd").toString } 105 | } 106 | } 107 | 108 | test("run in specified environment") { 109 | new Env(Map("newvar" -> "a")) { 110 | expectResult("a") { Shell("echo $newvar").toString } 111 | } 112 | } 113 | 114 | test("combine both env and pwd") { 115 | new Env(Map("newvar" -> "a")) { 116 | expectResult("a") { Shell("echo $newvar").toString } 117 | cd("/") { 118 | expectResult("a") { Shell("echo $newvar").toString } 119 | expectResult("/") { Command("pwd").toString } 120 | } 121 | 122 | // MacOS redirects /tmp to /private/tmp as canonical path 123 | val tmpPath = new java.io.File(System.getProperty("java.io.tmpdir")).getCanonicalPath 124 | val tmpdir = tmpPath.toString 125 | cd(tmpdir) { 126 | expectResult("a") { Shell("echo $newvar").toString } 127 | expectResult(tmpdir) { Command("pwd").toString } 128 | env(Map("newvar2" -> "b")) { 129 | expectResult(tmpdir) { Command("pwd").toString } 130 | cd("/") { 131 | expectResult("a") { Shell("echo $newvar").toString } 132 | expectResult("b") { Shell("echo $newvar2").toString } 133 | expectResult("/") { Command("pwd").toString } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | test("interactive IO") { 141 | val proc = Command("grep", "a") 142 | proc interact { io => 143 | import scala.concurrent.ExecutionContext.Implicits.global 144 | import at.loveoneanother.schale.actorSystem 145 | implicit val timeout = Timeout(2 seconds) 146 | 147 | io ! "a" 148 | io ! ProcStdinFlush 149 | io ! ProcStdinClose 150 | val future = io ? ProcStdoutReadLine 151 | future onComplete { 152 | case Success(line) => { 153 | expectResult("a") { line } 154 | expectResult(0) { proc.waitFor() } 155 | } 156 | case Failure(e) => 157 | ProcTest.this.fail("cannot read proc output") 158 | } 159 | Await.result(future, 4 seconds) 160 | } 161 | } 162 | 163 | test("interactive IO (read character from stderr)") { 164 | val proc = Shell("grep a 1>&2") 165 | proc interact { io => 166 | import scala.concurrent.ExecutionContext.Implicits.global 167 | import at.loveoneanother.schale.actorSystem 168 | implicit val timeout = Timeout(2 seconds) 169 | 170 | io ! "a" 171 | io ! ProcStdinFlush 172 | io ! ProcStdinClose 173 | val future = io ? ProcStderrReadChar 174 | future onComplete { 175 | case Success(char) => { 176 | expectResult('a') { char } 177 | expectResult(0) { proc.waitFor() } 178 | } 179 | case Failure(e) => 180 | ProcTest.this.fail("cannot read proc output") 181 | } 182 | Await.result(future, 4 seconds) 183 | } 184 | } 185 | 186 | test("IO redirect to file using shell") { 187 | new java.io.File("/tmp/schale_test").delete() 188 | Shell("echo a | grep a > /tmp/schale_test").waitFor() 189 | expectResult(true) { new java.io.File("/tmp/schale_test").exists() } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Schale 2 | ------ 3 | 4 | Schale is a subprocess interface for Scala. Make all your system programs easily available to Scala, call those programs and interact with their input and output! 5 | 6 | ## Why use Schale? 7 | 8 | Although Scala standard library `scala.sys.process` helps external process execution, however it does not: 9 | 10 | - Manage interactive input/output 11 | - Manage process environment (variables and working directory) 12 | 13 | Schale addresses both of the downsides: 14 | 15 | - It supports both asynchronous and synchronous interactive IO by exposing an Akka actor, eliminating the necessity of manual stream management. 16 | - Process environment can be very easily managed and prepared in a hierarchical fashion, reduce code redundancy and improve readability. 17 | 18 | ## Usage 19 | 20 | ### A gentle start 21 | 22 | import at.loveoneanother.schale._ 23 | println(Command("ls", "-l", "/")) 24 | println(Shell("ls -l /")) 25 | 26 | `Command` runs any process, `Shell` runs system shell interpreter. The function return values have exactly the same Schale API. 27 | 28 | This is the only use case in which `.waitFor()` call is unnecessary. 29 | 30 | ### Run in background 31 | 32 | val proc = Shell("sleep 100") 33 | proc.bg() 34 | val exitStatus = proc.waitFor() 35 | 36 | Both simple and interactive IO may be used on background process. 37 | 38 | Process `.destroy()` can be used any time to destory a process before its completion, even during simple or interactive IO. 39 | 40 | ### Simple IO 41 | 42 | val grepper = Shell("grep Scala") 43 | grepper.input("Java API\n", "Scala API\n", "Scala Doc\n") 44 | for (line <- grepper.stdout) { println(line) } 45 | for (line <- grepper.stderr) { println(line) } 46 | grepper.waitFor() 47 | 48 | `.input`, `.stdout` and `.stderr` may only be used once! 49 | 50 | ### Advanced (interactive) IO 51 | 52 | implicit val readTimeout = Timeout(2 seconds) 53 | Shell("grep Scala") interact { io => 54 | // Stdin: write chars and strings 55 | io ! "Java API"; io ! '\n' 56 | io ! "Scala API\n" 57 | io ! ProcStdinClose // EOF 58 | 59 | // Stdout: read chars and strings 60 | println(Await.result(io ? ProcStdoutReadChar, 2 seconds).asInstanceOf[Int].toChar) 61 | println(Await.result(io ? ProcStdoutReadLine, 2 seconds).asInstanceOf[String]) 62 | } waitFor 63 | 64 | `interact` may be used any number of times. For reading from error output, there are `ProcStderrReadChar` and `ProcStderrReadLine`. 65 | 66 | ### Process environment 67 | 68 | // Do not import implicit default environment 69 | import at.loveoneanother.schale.{ Env, Command, Shell } 70 | new Env() { 71 | cd("/") { 72 | println(Command("pwd")) // root 73 | env(Map("mylang" -> "Scala")) { println(Shell("echo $mylang")) } // Scala 74 | } 75 | cd("/tmp") { 76 | println(Command("pwd")) // tmp 77 | } 78 | env(Map("hobby" -> "gliding")) { 79 | // Override value 80 | env(Map("hobby" -> "programming")) { println(Shell("echo $hobby")) } // programming 81 | } 82 | } 83 | 84 | Start from a `new Env()` object, use `cd()` to change directory and `env()` to add/override variables, __stack__ those calls to easily manage hierarchical process environment! 85 | 86 | ## Installation 87 | `schale` currently uses [sbt-assembly](https://github.com/sbt/sbt-assembly) 88 | to produce a fat jar. 89 | First, verify `build.sbt` contains the same libraries as 90 | the codebase to integrate with. 91 | Next, run `sbt assembly` to create `target/scala-X.XX/schale.jar`. 92 | 93 | ## Some notes 94 | 95 | __Is `Shell()` cross platform?__ 96 | 97 | `Comamnd()` is cross platform, however `Shell("script")` assumes \*nix OS platform and the availability of `/bin/sh`. Alternative call `Shell("script", "interpreter path")` uses the specified interpreter, but still cannot be guaranteed to run cross-platform. 98 | 99 | __Can I `cd()` to relative paths?__ 100 | 101 | In a process environment, `cd()` call only accepts absolute path, this due to JVM's inability to determine whether a path is relative or absolute. You may find `java.nio.file.Paths` easy to work with `cd()`. 102 | 103 | __Can I use Schale to interact with SSH/SFTP/SCP?__ 104 | 105 | JVM cannot interact with TTY and Schale itself is not a terminal emulator, therefore it cannot be used on programs which require advanced terminal features, such as SSH/SFTP/SCP. Sorry! 106 | 107 | ## Version History 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
VersionBranchRelease DateComment
Alphaalpha2013-08-10First release
123 | 124 | ## Contact and License 125 | 126 | You may want to check out [Issues] section for future plans, and please feel very free to contact [Howard] if you have any feedback / questions. I also have [Twitter] and [blog], please check them out as well. 127 | 128 | The following copyright notice and disclaimers apply to all files in the project repository: 129 |
130 | Copyright (c) 2013, Howard Guo
131 | All rights reserved.
132 | 
133 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
134 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
135 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
136 | 
137 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
138 | 
139 | 140 | ## Project Background 141 | 142 | Subprocess management was traditionally carried out by using `Runtime.getRuntime().exec` series of calls. Although JDK introduced `ProcessBuilder` later on, but process building and IO interactivity could still be cumbersome. 143 | 144 | Schale takes advantage of advanced features and syntactic sugar offered by Scala, and brings to you: 145 | 146 | - Easy process creation 147 | - Background process management 148 | - Simplified process input/output interface 149 | - Advanced, interactive, non-blocking process IO 150 | - Hierarchical process environment (variables, working directory) management 151 | 152 | Schale was inspired by Python third-party package "sh". 153 | 154 | [Howard]: mailto:guohouzuo@gmail.com 155 | [Twitter]: https://twitter.com/hzguo 156 | [blog]: http://allstarnix.blogspot.com.au 157 | [Issues]: https://github.com/HouzuoGuo/schale/issues 158 | --------------------------------------------------------------------------------