diff --git a/sbt/LICENSE b/sbt/LICENSE index 6d1f89b26..98023c493 100644 --- a/sbt/LICENSE +++ b/sbt/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2008, 2009, 2010 Steven Blundy, Josh Cough, Nathan Hamblen, Mark Harrah, David MacIver, Mikko Peltonen, Tony Sloane, Vesa Vilhonen +Copyright (c) 2008, 2009, 2010 Steven Blundy, Josh Cough, Nathan Hamblen, Mark Harrah, David MacIver, Mikko Peltonen, Tony Sloane, Seth Tisue, Vesa Vilhonen All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/sbt/src/main/scala/sbt/Main.scala b/sbt/src/main/scala/sbt/Main.scala index fa2e97d72..2d5129018 100755 --- a/sbt/src/main/scala/sbt/Main.scala +++ b/sbt/src/main/scala/sbt/Main.scala @@ -156,7 +156,7 @@ class xMain extends xsbti.AppMain arguments match { case "" :: tail => continue(project, tail, failAction) - case x :: tail if x.startsWith(";") => continue(project, x.split(";").toList ::: arguments, failAction) + case x :: tail if x.startsWith(";") => continue(project, x.split(";").toList ::: tail, failAction) case (ExitCommand | QuitCommand) :: _ => result( Exit(NormalExitCode) ) case RebootCommand :: tail => reload( tail ) case InteractiveCommand :: _ => continue(project, prompt(baseProject, project) :: arguments, interactiveContinue) diff --git a/sbt/src/main/scala/sbt/Process.scala b/sbt/src/main/scala/sbt/Process.scala index 87aa314c8..a26635230 100644 --- a/sbt/src/main/scala/sbt/Process.scala +++ b/sbt/src/main/scala/sbt/Process.scala @@ -101,6 +101,28 @@ trait Process extends NotNull /** Represents a runnable process. */ trait ProcessBuilder extends SourcePartialBuilder with SinkPartialBuilder { + /** Starts the process represented by this builder, blocks until it exits, and returns the output as a String. Standard error is + * sent to the console. If the exit code is non-zero, an exception is thrown.*/ + def !! : String + /** Starts the process represented by this builder, blocks until it exits, and returns the output as a String. Standard error is + * sent to the provided Logger. If the exit code is non-zero, an exception is thrown.*/ + def !!(log: Logger) : String + /** Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the console. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination and then throw an exception. */ + def lines: Stream[String] + /** Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the provided Logger. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination but will not throw an exception. */ + def lines(log: Logger): Stream[String] + /** Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the console. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination but will not throw an exception. */ + def lines_! : Stream[String] + /** Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available + * but the process has not completed. Standard error is sent to the provided Logger. If the process exits with a non-zero value, + * the Stream will provide all lines up to termination but will not throw an exception. */ + def lines_!(log: Logger): Stream[String] /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are * sent to the console.*/ def ! : Int @@ -108,10 +130,10 @@ trait ProcessBuilder extends SourcePartialBuilder with SinkPartialBuilder * sent to the given Logger.*/ def !(log: Logger): Int /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are - * sent to the console. The newly started process reads from standard input of the current process if `connectInput` is true.*/ + * sent to the console. The newly started process reads from standard input of the current process.*/ def !< : Int /** Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are - * sent to the given Logger. The newly started process reads from standard input of the current process if `connectInput` is true.*/ + * sent to the given Logger. The newly started process reads from standard input of the current process.*/ def !<(log: Logger) : Int /** Starts the process represented by this builder. Standard output and error are sent to the console.*/ def run(): Process diff --git a/sbt/src/main/scala/sbt/impl/ProcessImpl.scala b/sbt/src/main/scala/sbt/impl/ProcessImpl.scala index 993c9068f..67e6b187a 100644 --- a/sbt/src/main/scala/sbt/impl/ProcessImpl.scala +++ b/sbt/src/main/scala/sbt/impl/ProcessImpl.scala @@ -43,17 +43,24 @@ private object Future private object BasicIO { + def apply(buffer: StringBuffer, log: Option[Logger], withIn: Boolean) = new ProcessIO(input(withIn), processFully(buffer), getErr(log)) def apply(log: Logger, withIn: Boolean) = new ProcessIO(input(withIn), processFully(log, Level.Info), processFully(log, Level.Error)) + def getErr(log: Option[Logger]) = log match { case Some(lg) => processFully(lg, Level.Error); case None => toStdErr } + def ignoreOut = (i: OutputStream) => () - val BufferSize = 8192 + final val BufferSize = 8192 + final val Newline = FileUtilities.Newline + def close(c: java.io.Closeable) = try { c.close() } catch { case _: java.io.IOException => () } - def processFully(log: Logger, level: Level.Value)(i: InputStream) { processFully(line => log.log(level, line))(i) } - def processFully(processLine: String => Unit)(i: InputStream) - { - val reader = new BufferedReader(new InputStreamReader(i)) - processLinesFully(processLine)(reader.readLine) - } + def processFully(log: Logger, level: Level.Value): InputStream => Unit = processFully(line => log.log(level, line)) + def processFully(buffer: Appendable): InputStream => Unit = processFully(appendLine(buffer)) + def processFully(processLine: String => Unit): InputStream => Unit = + in => + { + val reader = new BufferedReader(new InputStreamReader(in)) + processLinesFully(processLine)(reader.readLine) + } def processLinesFully(processLine: String => Unit)(readLine: () => String) { def readFully() @@ -70,12 +77,22 @@ private object BasicIO def connectToIn(o: OutputStream) { transferFully(System.in, o) } def input(connect: Boolean): OutputStream => Unit = if(connect) connectToIn else ignoreOut def standard(connectInput: Boolean): ProcessIO = standard(input(connectInput)) - def standard(in: OutputStream => Unit): ProcessIO = new ProcessIO(in, transferFully(_, System.out), transferFully(_, System.err)) + def standard(in: OutputStream => Unit): ProcessIO = new ProcessIO(in, toStdOut, toStdErr) + + def toStdErr = (in: InputStream) => transferFully(in, System.err) + def toStdOut = (in: InputStream) => transferFully(in, System.out) def transferFully(in: InputStream, out: OutputStream): Unit = try { transferFullyImpl(in, out) } catch { case _: InterruptedException => () } - + + private[this] def appendLine(buffer: Appendable): String => Unit = + line => + { + buffer.append(line) + buffer.append(Newline) + } + private[this] def transferFullyImpl(in: InputStream, out: OutputStream) { val continueCount = 1//if(in.isInstanceOf[PipedInputStream]) 1 else 0 @@ -113,7 +130,31 @@ private abstract class AbstractProcessBuilder extends ProcessBuilder with SinkPa def run(connectInput: Boolean): Process = run(BasicIO.standard(connectInput)) def run(log: Logger): Process = run(log, false) def run(log: Logger, connectInput: Boolean): Process = run(BasicIO(log, connectInput)) - + + private[this] def getString(log: Option[Logger], withIn: Boolean): String = + { + val buffer = new StringBuffer + val code = this ! BasicIO(buffer, log, withIn) + if(code == 0) buffer.toString else error("Nonzero exit value: " + code) + } + def !! = getString(None, false) + def !!(log: Logger) = getString(Some(log), false) + def !!< = getString(None, true) + def !!<(log: Logger) = getString(Some(log), true) + + def lines: Stream[String] = lines(false, true, None) + def lines(log: Logger): Stream[String] = lines(false, true, Some(log)) + def lines_! : Stream[String] = lines(false, false, None) + def lines_!(log: Logger): Stream[String] = lines(false, false, Some(log)) + + private[this] def lines(withInput: Boolean, nonZeroException: Boolean, log: Option[Logger]): Stream[String] = + { + val streamed = Streamed[String](nonZeroException) + val process = run(new ProcessIO(BasicIO.input(withInput), BasicIO.processFully(streamed.process), BasicIO.getErr(log))) + Spawn { streamed.done(process.exitValue()) } + streamed.stream() + } + def ! = run(false).exitValue() def !< = run(true).exitValue() def !(log: Logger) = runBuffered(log, false) @@ -413,3 +454,20 @@ object Uncloseable def protect(in: InputStream): InputStream = if(in eq System.in) Uncloseable(in) else in def protect(out: OutputStream): OutputStream = if( (out eq System.out) || (out eq System.err)) Uncloseable(out) else out } +private object Streamed +{ + def apply[T](nonzeroException: Boolean): Streamed[T] = + { + val q = new java.util.concurrent.LinkedBlockingQueue[Either[Int, T]] + def next(): Stream[T] = + q.take match + { + case Left(0) => Stream.empty + case Left(code) => if(nonzeroException) error("Nonzero exit code: " + code) else Stream.empty + case Right(s) => Stream.cons(s, next) + } + new Streamed((s: T) => q.put(Right(s)), code => q.put(Left(code)), () => next()) + } +} + +private final class Streamed[T](val process: T => Unit, val done: Int => Unit, val stream: () => Stream[T]) extends NotNull \ No newline at end of file