diff --git a/compile/api/SameAPI.scala b/compile/api/SameAPI.scala index 88f76524a..b52492e03 100644 --- a/compile/api/SameAPI.scala +++ b/compile/api/SameAPI.scala @@ -44,11 +44,11 @@ object SameAPI { val start = System.currentTimeMillis - println("\n=========== API #1 ================") + /*println("\n=========== API #1 ================") import DefaultShowAPI._ println(ShowAPI.show(a)) println("\n=========== API #2 ================") - println(ShowAPI.show(b)) + println(ShowAPI.show(b))*/ /** de Bruijn levels for type parameters in source a and b*/ val tagsA = TagTypeVariables(a) diff --git a/compile/discover/Discovered.scala b/compile/discover/Discovered.scala index 7d883edac..068be9e83 100644 --- a/compile/discover/Discovered.scala +++ b/compile/discover/Discovered.scala @@ -5,6 +5,9 @@ package sbt package inc final case class Discovered(baseClasses: Set[String], annotations: Set[String], hasMain: Boolean, isModule: Boolean) +{ + def isEmpty = baseClasses.isEmpty && annotations.isEmpty +} object Discovered { def empty = new Discovered(Set.empty, Set.empty, false, false) diff --git a/main/Command.scala b/main/Command.scala index 94fd58887..4d31db162 100644 --- a/main/Command.scala +++ b/main/Command.scala @@ -4,33 +4,68 @@ package sbt import Execute.NodeView +import Completions.noCompletions sealed trait Command { def applies: State => Option[Apply] } +trait UserCommand extends Command trait Apply { - def help: Seq[(String,String)] + def help: Seq[Help] def run: Input => Option[State] + def complete: Input => Completions +} +trait Help +{ + def detail: (Set[String], String) + def brief: (String, String) +} +object Help +{ + def apply(briefHelp: (String, String), detailedHelp: (Set[String], String) = (Set.empty, "") ): Help = new Help { def detail = detailedHelp; def brief = briefHelp } +} +trait Completions +{ + def candidates: Seq[String] + def position: Int +} +object Completions +{ + implicit def seqToCompletions(s: Seq[String]): Completions = apply(s : _*) + def apply(s: String*): Completions = new Completions { def candidates = s; def position = 0 } + + def noCompletions: PartialFunction[Input, Completions] = { case _ => Completions() } } object Command { + def univ(f: State => Apply): Command = direct(s => Some(f(s))) def direct(f: State => Option[Apply]): Command = new Command { def applies = f } def apply(f: PartialFunction[State, Apply]): Command = direct(f.lift) - def simple(name: String, help: (String, String)*)(f: State => State): Command = - apply { case s => Apply(help) { case in if in.line == name => f(s) }} + def simple(name: String, brief: (String, String), detail: String)(f: (Input, State) => State): Command = + univ( Apply.simple(name, brief, detail)(f) ) } object Apply { - def direct(h: Seq[(String,String)])(r: Input => Option[State]): Apply = - new Apply { def help = h; def run = r } - - def apply(h: Seq[(String,String)])(r: PartialFunction[Input, State]): Apply = - direct(h)(r.lift) + def direct(h: Help*)(c: Input => Completions = noCompletions )(r: Input => Option[State]): Apply = + new Apply { def help = h; def run = r; def complete = c } + + def apply(h: Help*)(r: PartialFunction[Input, State]): Apply = + direct(h : _*)( noCompletions )(r.lift) + /* Currently problematic for apply( Seq(..): _*)() (...) + * TODO: report bug and uncomment when fixed + def apply(h: Help*)(complete: PartialFunction[Input, Completions] = noCompletions )(r: PartialFunction[Input, State]): Apply = + direct(h : _*)( complete orElse noCompletions )(r.lift)*/ + + def simple(name: String, brief: (String, String), detail: String)(f: (Input, State) => State): State => Apply = + { + val h = Help(brief, (Set(name), detail) ) + s => Apply( h ){ case in if name == in.name => f( in, s) } + } } trait Logged @@ -41,22 +76,34 @@ trait HistoryEnabled { def historyPath: Option[Path] } +trait Named +{ + def name: String +} +trait Member[Node <: Member[Node]] +{ self: Node => + def projectClosure: Seq[Node] +} trait Tasked { type Task[T] <: AnyRef def task(name: String, state: State): Option[Task[State]] implicit def taskToNode: NodeView[Task] - def help: Seq[(String, String)] + def help: Seq[Help] } trait TaskSetup { def maxThreads = Runtime.getRuntime.availableProcessors def checkCycles = false } -final case class Input(line: String) +final case class Input(line: String, cursor: Option[Int]) { - def name: String = error("TODO") - def arguments: String = error("TODO") + lazy val (name, arguments) = line match { case Input.NameRegex(n, a) => (n, a); case _ => (line, "") } + lazy val splitSpace = (arguments split """\s+""").toSeq +} +object Input +{ + val NameRegex = """\s*(\p{Punct}+|[\w-]+)\s*(.*)""".r } object Next extends Enumeration { diff --git a/main/CommandSupport.scala b/main/CommandSupport.scala new file mode 100644 index 000000000..e56e2e122 --- /dev/null +++ b/main/CommandSupport.scala @@ -0,0 +1,157 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt + +import complete.HistoryCommands +import scala.annotation.tailrec + +import java.io.File +import Path._ + +object CommandSupport +{ + def logger(s: State) = s match { + case State(p: Logged) => p.log + case _ => new ConsoleLogger //TODO: add a default logger to State + } + def notReadable(files: Seq[File]): Seq[File] = files filter { !_.canRead } + def readable(files: Seq[File]): Seq[File] = files filter { _.canRead } + def sbtRCs(s: State): Seq[File] = + (Path.userHome / sbtrc) :: + (s.baseDir / sbtrc asFile) :: + Nil + + def readLines(files: Seq[File]): Seq[String] = files flatMap (line => IO.readLines(line)) flatMap processLine + def processLine(s: String) = { val trimmed = s.trim; if(ignoreLine(trimmed)) None else Some(trimmed) } + def ignoreLine(s: String) = s.isEmpty || s.startsWith("#") + + val HelpCommand = "help" + val ProjectCommand = "project" + val ProjectsCommand = "projects" + + val Exit = "exit" + val Quit = "quit" + + /** The list of command names that may be used to terminate the program.*/ + val TerminateActions: Seq[String] = Seq(Exit, Quit) + + def helpBrief = (HelpCommand + " command*", "Displays this help message or prints detailed help on requested commands.") + def helpDetailed = "If an argument is provided, this prints detailed help for that command.\nOtherwise, this prints a help summary." + + def projectBrief = (ProjectCommand + " [project]", "Displays the current project or changes to the provided `project`.") + def projectDetailed = +ProjectCommand + +""" + Displays the name of the current project. + +""" + ProjectCommand + """ name + Changes to the project with the provided name. + This command fails if there is no project with the given name. +""" + + def projectsBrief = (ProjectsCommand, projectsDetailed) + def projectsDetailed = "Displays the names of available projects." + + def historyHelp = HistoryCommands.descriptions.map( d => Help(d) ) + + def exitBrief = (TerminateActions.mkString(", "), "Terminates the build.") + + def sbtrc = ".sbtrc" + + def ReadCommand = "<" + def ReadFiles = " file1 file2 ..." + def ReadBrief = (ReadCommand + " file*", "Reads command lines from the provided files.") + def ReadDetailed = ReadCommand + ReadFiles + +""" + Reads the lines from the given files and inserts them as commands. + Any lines that are empty or that start with # are ignored. + If a file does not exist or is not readable, this command fails. + + All commands are read before any are executed. + Therefore, if any file is not readable, no commands from any files will be + run. + + You probably need to escape this command if entering it at your shell. +""" + + def DefaultsCommand = "add-default-commands" + def DefaultsBrief = (DefaultsCommand, DefaultsDetailed) + def DefaultsDetailed = "Registers default built-in commands" + + def ReloadCommand = "reload" + def ReloadBrief = (ReloadCommand, "Reloads the session and continues to execute the remaining commands.") + def ReloadDetailed = +ReloadCommand + """ + This command is equivalent to exiting, restarting, and running the + remaining commands with the exception that the jvm is not shut down. +""" + + def Multi = ";" + def MultiBrief = ("( " + Multi + " command )+", MultiDetailed) + def MultiDetailed = "Runs the provided semicolon-separated commands." + + val AliasCommand = "alias" + def AliasBrief = (AliasCommand, "Adds, removes, or prints command aliases.") + def AliasDetailed = +AliasCommand + """ + Prints a list of defined aliases. + +""" + +AliasCommand + """ name + Prints the alias defined for `name`. + +""" + +AliasCommand + """ name=value + Sets the alias `name` to `value`, replacing any existing alias with that name. + Whenever `name` is entered, value is run. + If any arguments are provided to `name`, those are appended to `value`. + +""" + +AliasCommand + """ name= + Removes the alias for `name`. +""" + + def Load = "load" + def LoadLabel = "a project" + def LoadCommand = "load-commands" + def LoadCommandLabel = "commands" + + def Shell = "shell" + def ShellBrief = (Shell, ShellDetailed) + def ShellDetailed = "Provides an interactive prompt from which commands can be run." + + def OnFailure = "-" + def OnFailureBrief = (OnFailure + " command", "Registers 'command' to run if a command fails.") + def OnFailureDetailed = +OnFailure + """ command + Registers 'command' to run when a command fails to complete normally. + Only one failure command may be registered at a time, so this + command replaces the previous command if there is one. + The failure command is reset when it runs, so it must be added again + if desired. +""" + + def IfLast = "iflast" + def IfLastBrief = (IfLast + " command", IfLastDetailed) + def IfLastDetailed = "If there are no more commands after this one, 'command' is run." + + def InitCommand = "initialize" + def InitBrief = (InitCommand, "Initializes command processing.") + def InitDetailed = +InitCommand + """ + Initializes command processing. + +Runs the following commands. + +defaults + Registers default commands. + +load-commands -base ~/.sbt/commands + Builds and loads command definitions from ~/.sbt/commands + +< ~/.sbtrc +< .sbtrc + Runs commands from ~/.sbtrc and ./.sbtrc if they exist +""" +} \ No newline at end of file diff --git a/main/Main.scala b/main/Main.scala index c6d450316..e5cf25b04 100644 --- a/main/Main.scala +++ b/main/Main.scala @@ -6,17 +6,23 @@ package sbt import Execute.NodeView import complete.HistoryCommands import HistoryCommands.{Start => HistoryPrefix} -import sbt.build.{AggressiveCompile, Build, BuildException, Parse, ParseException} +import sbt.build.{AggressiveCompile, Auto, Build, BuildException, LoadCommand, Parse, ParseException, ProjectLoad, SourceLoad} import scala.annotation.tailrec +import scala.collection.JavaConversions._ +import Path._ + +import java.io.File /** This class is the entry point for sbt.*/ class xMain extends xsbti.AppMain { final def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = { - import Commands._ - val initialCommands = Seq(help, history, exit, load) - val state = State( () )( configuration, initialCommands, Set.empty, None, configuration.arguments.map(_.trim).toList, Next.Continue ) + import Commands.{initialize, defaults} + import CommandSupport.{DefaultsCommand, InitCommand} + val initialCommandDefs = Seq(initialize, defaults) + val commands = DefaultsCommand :: InitCommand :: configuration.arguments.map(_.trim).toList + val state = State( () )( configuration, initialCommandDefs, Set.empty, None, commands, Next.Continue ) run(state) } @@ -36,53 +42,144 @@ class xMain extends xsbti.AppMain def next(state: State): State = state.process(process) def process(command: String, state: State): State = { - val in = Input(command) + val in = Input(command, None) Commands.applicable(state).flatMap( _.run(in) ).headOption.getOrElse { System.err.println("Unknown command '" + command + "'") state.fail } } - } - +import CommandSupport._ object Commands { + def DefaultCommands = Seq(help, reload, read, history, exit, load, loadCommands, projects, project, setOnFailure, ifLast, multi, shell, alias, act) + def applicable(state: State): Stream[Apply] = state.processors.toStream.flatMap(_.applies(state) ) + + def detail(selected: Iterable[String])(h: Help): Option[String] = + h.detail match { case (commands, value) => if( selected exists commands ) Some(value) else None } + + def help = Command.simple(HelpCommand, helpBrief, helpDetailed) { (in, s) => + + val h = applicable(s).flatMap(_.help) + val argStr = (in.line stripPrefix HelpCommand).trim - def help = Command.simple("help", ("help", "Displays this help message.")) { s => - val message = applicable(s).flatMap(_.help).map { case (a,b) => a + " : " + b }.mkString("\n") + val message = + if(argStr.isEmpty) + h.map( _.brief match { case (a,b) => a + " : " + b } ).mkString("\n", "\n", "\n") + else + h flatMap detail( argStr.split("""\s+""", 0) ) mkString("\n", "\n\n", "\n") System.out.println(message) s } + def alias = Command.simple(AliasCommand, AliasBrief, AliasDetailed) { (in, s) => + in.arguments.split("""\s*=\s*""",2).toSeq match { + case Seq(name, value) => addAlias(s, name.trim, value.trim) + case Seq(x) if !x.isEmpty=> printAlias(s, x.trim); s + case _ => printAliases(s); s + } + } + + def shell = Command.simple(Shell, ShellBrief, ShellDetailed) { (in, s) => + val historyPath = s.project match { case he: HistoryEnabled => he.historyPath; case _ => None } + val reader = new LazyJLineReader(historyPath, new LazyCompletor(completor(s))) + val line = reader.readLine("> ") + line match { case Some(line) => line :: Shell :: s; case None => s } + } + + def multi = Command.simple(Multi, MultiBrief, MultiDetailed) { (in, s) => + in.arguments.split(";").toSeq ::: s + } + + def ifLast = Command.simple(IfLast, IfLastBrief, IfLastDetailed) { (in, s) => + if(s.commands.isEmpty) in.arguments :: s else s + } + + def setOnFailure = Command.simple(OnFailure, OnFailureBrief, OnFailureDetailed) { (in, s) => + s.copy()(onFailure = Some(in.arguments)) + } + + def reload = Command.simple(ReloadCommand, ReloadBrief, ReloadDetailed) { (in, s) => + runExitHooks(s).reload + } + + def defaults = Command.simple(DefaultsCommand, DefaultsBrief, DefaultsDetailed) { (in, s) => + s ++ DefaultCommands + } + + def initialize = Command.simple(InitCommand, InitBrief, InitDetailed) { (in, s) => + readLines( readable( sbtRCs(s) ) ) ::: s + } + + def read = Command.simple(ReadCommand, ReadBrief, ReadDetailed) { (in, s) => + val from = in.splitSpace map { p => new File(s.baseDir, p) } + val notFound = notReadable(from) + if(notFound.isEmpty) + readLines(from) ::: s // this means that all commands from all files are loaded, parsed, and inserted before any are executed + else { + logger(s).error("File(s) not readable: \n\t" + notFound.mkString("\n\t")) + s + } + } + def history = Command { case s @ State(p: HistoryEnabled with Logged) => - Apply(HistoryCommands.descriptions) { + Apply( historyHelp: _* ) { case in if in.line startsWith("!") => HistoryCommands(in.line.substring(HistoryPrefix.length).trim, p.historyPath, 500/*JLine.MaxHistorySize*/, p.log.error _) match { case Some(commands) => - commands.foreach(println) //better to print it than to log it + commands.foreach(println) //printing is more appropriate than logging (commands ::: s).continue case None => s.fail } } } + + def listProject(p: Named, log: Logger) = printProject("\t", p, log) + def printProject(prefix: String, p: Named, log: Logger) = log.info(prefix + p.name) + + def projects = Command { case s @ State(d: Member[_]) => + Apply.simple(ProjectsCommand, projectsBrief, projectsDetailed ) { (in,s) => + val log = logger(s) + d.projectClosure.foreach { case n: Named => listProject(n, log) } + s + }(s) + } + + def project = Command { case s @ State(d: Member[_] with Named) => + Apply.simple(ProjectCommand, projectBrief, projectDetailed ) { (in,s) => + val to = in.arguments + if(to.isEmpty) + { + logger(s).info(d.name) + s + } + else + { + d.projectClosure.find { case n: Named => n.name == to; case _ => false } match + { + case Some(np) => logger(s).info("Set current project to " + to); s.copy(np)() + case None => logger(s).error("Invalid project name '" + to + "' (type 'projects' to list available projects)."); s.fail + } + } + }(s) + } - def helpExit = (TerminateActions.mkString(", "), "Terminates the build.") - - def exit = Command { case s => Apply(helpExit :: Nil) { - case Input(line) if TerminateActions contains line => - s.exit(true) + def exit = Command { case s => Apply( Help(exitBrief) ) { + case in if TerminateActions contains in.line => + runExitHooks(s).exit(true) } } def act = Command { case s @ State(p: Tasked) => new Apply { def help = p.help + def complete = in => Completions() def run = in => { - lazy val (checkCycles, maxThreads) = p match { + val (checkCycles, maxThreads) = p match { case c: TaskSetup => (c.checkCycles, c.maxThreads) case _ => (false, Runtime.getRuntime.availableProcessors) } @@ -91,28 +188,44 @@ object Commands } } } - - def load = Command { case s => Apply(Nil) { - case Input(line) if line.startsWith("load") => - loadCommand(line, s.configuration) match - { - case Right(newValue) => - ExitHooks.runExitHooks(s.exitHooks.toSeq) - s.copy(project = newValue)(exitHooks = Set.empty) - case Left(e) => e.printStackTrace; System.err.println(e.toString); s.fail // TODO: log instead of print - } + + def load = Command.simple(Load, Parse.helpBrief(Load, LoadLabel), Parse.helpDetail(Load, LoadLabel, false) ) { (in, s) => + loadCommand(in.arguments, s.configuration, false, "sbt.Project") match // TODO: classOf[Project].getName when ready + { + case Right(Seq(newValue)) => runExitHooks(s).copy(project = newValue)() + case Left(e) => e.printStackTrace; System.err.println(e.toString); s.fail // TODO: log instead of print } } - def loadCommand(line: String, configuration: xsbti.AppConfiguration): Either[Throwable, Any] = - try { Right( Build( Parse(line)(configuration.baseDirectory), configuration ) ) } - catch { case e @ (_: ParseException | _: BuildException | _: xsbti.CompileFailed) => Left(e) } - - val Exit = "exit" - val Quit = "quit" - /** The list of command names that may be used to terminate the program.*/ - val TerminateActions: Seq[String] = Seq(Exit, Quit) + def runExitHooks(s: State): State = { + ExitHooks.runExitHooks(s.exitHooks.toSeq) + s.copy()(exitHooks = Set.empty) + } + def loadCommands = Command.simple(LoadCommand, Parse.helpBrief(LoadCommand, LoadCommandLabel), Parse.helpDetail(LoadCommand, LoadCommandLabel, true) ) { (in, s) => + loadCommand(in.arguments, s.configuration, true, classOf[UserCommand].getName) match + { + case Right(newCommands) => + val asCommands = newCommands map { case c: Command => c; case x => error("Not a Command: " + x.asInstanceOf[AnyRef].getClass) } + s.copy()(processors = asCommands ++ s.processors) + case Left(e) => e.printStackTrace; System.err.println(e.toString); s.fail // TODO: log instead of print + } + } + + def loadCommand(line: String, configuration: xsbti.AppConfiguration, allowMultiple: Boolean, defaultSuper: String): Either[Throwable, Seq[Any]] = + try + { + val parsed = Parse(line)(configuration.baseDirectory) + Right( Build( translateEmpty(parsed, defaultSuper), configuration, allowMultiple) ) + } + catch { case e @ (_: ParseException | _: BuildException | _: xsbti.CompileFailed) => Left(e) } + + def translateEmpty(load: LoadCommand, defaultSuper: String): LoadCommand = + load match { + case ProjectLoad(base, Auto.Explicit, "") => ProjectLoad(base, Auto.Subclass, defaultSuper) + case s @ SourceLoad(_, _, _, _, Auto.Explicit, "") => s.copy(auto = Auto.Subclass, name = defaultSuper) + case x => x + } def runTask[Task[_] <: AnyRef](root: Task[State], checkCycles: Boolean, maxWorkers: Int)(implicit taskToNode: NodeView[Task]): Result[State] = { @@ -127,6 +240,52 @@ object Commands case Value(v) => v case Inc(Incomplete(tpe, message, causes, directCause)) => // tpe: IValue = Error, message: Option[String] = None, causes: Seq[Incomplete] = Nil, directCause: Option[Throwable] = None) println("Task did not complete successfully (TODO: error logging)") + directCause.foreach(_.printStackTrace) original } + + def completor(s: State): jline.Completor = new jline.Completor { + lazy val apply = applicable(s) + def complete(buffer: String, cursor: Int, candidates: java.util.List[_]): Int = + { + val correct = candidates.asInstanceOf[java.util.List[String]] + val in = Input(buffer, Some(cursor)) + val completions = apply.map(_.complete(in)) + val maxPos = if(completions.isEmpty) -1 else completions.map(_.position).max + correct ++= ( completions flatMap { c => if(c.position == maxPos) c.candidates else Nil } ) + maxPos + } + } + def addAlias(s: State, name: String, value: String): State = + { + val in = Input(name, None) + if(in.name == name) { + val removed = removeAlias(s, name) + if(value.isEmpty) removed else removed.copy()(processors = new Alias(name, value) +: removed.processors) + } else { + System.err.println("Invalid alias name '" + name + "'.") + s.fail + } + } + def removeAlias(s: State, name: String): State = + s.copy()(processors = s.processors.filter { case a: Alias if a.name == name => false; case _ => true } ) + + def printAliases(s: State): Unit = { + val strings = aliasStrings(s) + if(!strings.isEmpty) println( strings.mkString("\t", "\n\t","") ) + } + + def printAlias(s: State, name: String): Unit = + for(a <- aliases(s)) if (a.name == name) println("\t" + name + " = " + a.value) + + def aliasStrings(s: State) = aliases(s).map(a => a.name + " = " + a.value) + def aliases(s: State) = s.processors collect { case a: Alias => a } + + final class Alias(val name: String, val value: String) extends UserCommand { + assert(name.length > 0) + assert(value.length > 0) + def applies = s => Some(Apply() { + case in if in.name == name=> (value + " " + in.arguments) :: s + }) + } } \ No newline at end of file diff --git a/main/State.scala b/main/State.scala index 2cb7eb016..a153ddea3 100644 --- a/main/State.scala +++ b/main/State.scala @@ -3,6 +3,8 @@ */ package sbt +import java.io.File + case class State(project: Any)( val configuration: xsbti.AppConfiguration, val processors: Seq[Command], @@ -20,18 +22,24 @@ trait StateOps { def reload: State def exit(ok: Boolean): State def fail: State + def ++ (newCommands: Seq[Command]): State + def + (newCommand: Command): State + def baseDir: File } object State { implicit def stateOps(s: State): StateOps = new StateOps { def process(f: (String, State) => State): State = s.commands match { - case x :: xs => f(x, s.copy()(commands = xs)) - case Nil => exit(true) + case Seq(x, xs @ _*) => f(x, s.copy()(commands = xs)) + case Seq() => exit(true) } s.copy()(commands = s.commands.drop(1)) def ::: (newCommands: Seq[String]): State = s.copy()(commands = newCommands ++ s.commands) - def :: (command: String): State = s.copy()(commands = command +: s.commands) + def :: (command: String): State = (command :: Nil) ::: this + def ++ (newCommands: Seq[Command]): State = s.copy()(processors = s.processors ++ newCommands) + def + (newCommand: Command): State = this ++ (newCommand :: Nil) + def baseDir: File = s.configuration.baseDirectory def setNext(n: Next.Value) = s.copy()(next = n) def continue = setNext(Next.Continue) def reload = setNext(Next.Reload) diff --git a/main/build/Build.scala b/main/build/Build.scala index ec9656470..09d9cdd3f 100644 --- a/main/build/Build.scala +++ b/main/build/Build.scala @@ -8,6 +8,7 @@ import java.io.File import classpath.ClasspathUtilities.toLoader import ModuleUtilities.getObject import compile.{AnalyzingCompiler, JavaCompiler} +import inc.Analysis import Path._ import GlobFilter._ @@ -18,43 +19,36 @@ object Build def loader(configuration: xsbti.AppConfiguration): ClassLoader = configuration.provider.mainClass.getClassLoader - def apply(command: LoadCommand, configuration: xsbti.AppConfiguration): Any = + def apply(command: LoadCommand, configuration: xsbti.AppConfiguration, allowMultiple: Boolean = false): Seq[Any] = command match { case BinaryLoad(classpath, module, name) => - binary(classpath, module, name, loader(configuration)) + binary(classpath, module, name, loader(configuration), allowMultiple) case SourceLoad(classpath, sourcepath, output, module, auto, name) => - source(classpath, sourcepath, output, module, auto, name, configuration) - case ProjectLoad(base, name) => - project(base, name, configuration) + source(classpath, sourcepath, output, module, auto, name, configuration, allowMultiple)._1 + case ProjectLoad(base, auto, name) => + project(base, auto, name, configuration, allowMultiple)._1 } - def project(base: File, name: String, configuration: xsbti.AppConfiguration): Any = + def project(base: File, auto: Auto.Value, name: String, configuration: xsbti.AppConfiguration, allowMultiple: Boolean = false): (Seq[Any], Analysis) = { - val nonEmptyName = if(name.isEmpty) "Project" else name val buildDir = base / "project" / "build" val sources = buildDir * "*.scala" +++ buildDir / "src" / "main" / "scala" ** "*.scala" - source(Nil, sources.get.toSeq, Some(buildDir / "target" asFile), false, Auto.Explicit, nonEmptyName, configuration) + source(Nil, sources.get.toSeq, Some(buildDir / "target" asFile), false, auto, name, configuration, allowMultiple) } - def binary(classpath: Seq[File], module: Boolean, name: String, parent: ClassLoader): Any = + def binary(classpath: Seq[File], module: Boolean, name: String, parent: ClassLoader, allowMultiple: Boolean = false): Seq[Any] = { if(name.isEmpty) error("Class name required to load binary project.") else { - val loader = toLoader(classpath, parent) - if(module) - getObject(name, loader) - else - { - val clazz = Class.forName(name, true, loader) - clazz.newInstance - } + val names = if(allowMultiple) name.split(",").toSeq else Seq(name) + binaries(classpath, module, names, parent) } } - def source(classpath: Seq[File], sources: Seq[File], output: Option[File], module: Boolean, auto: Auto.Value, name: String, configuration: xsbti.AppConfiguration): Any = + def source(classpath: Seq[File], sources: Seq[File], output: Option[File], module: Boolean, auto: Auto.Value, name: String, configuration: xsbti.AppConfiguration, allowMultiple: Boolean = false): (Seq[Any], Analysis) = { // TODO: accept Logger as an argument val log = new ConsoleLogger with Logger with sbt.IvyLogger @@ -78,7 +72,9 @@ object Build val analysis = agg(compiler, javac, sources, compileClasspath, outputDirectory, Nil, Nil)(log) val discovered = discover(analysis, module, auto, name) - load(discovered)(x => binary(projectClasspath, module, x, loader(configuration)) ) + val loaded = binaries(projectClasspath, module, check(discovered, allowMultiple), loader(configuration)) + + (loaded, analysis) } def discover(analysis: inc.Analysis, module: Boolean, auto: Auto.Value, name: String): Seq[String] = { @@ -92,16 +88,33 @@ object Build def discover(analysis: inc.Analysis, module: Boolean, discovery: inc.Discovery): Seq[String] = { for(src <- analysis.apis.internal.values.toSeq; - (df, found) <- discovery(src.definitions) if found.isModule == module) + (df, found) <- discovery(src.definitions) if !found.isEmpty && found.isModule == module) yield df.name } - def load(discovered: Seq[String])(doLoad: String => Any): Any = + + def binaries(classpath: Seq[File], module: Boolean, names: Seq[String], parent: ClassLoader): Seq[Any] = + loadBinaries(names, module, toLoader(classpath, parent)) + + def loadBinaries(names: Seq[String], module: Boolean, loader: ClassLoader): Seq[Any] = + for(name <- names if !name.isEmpty) yield + loadBinary(name, module, loader) + + def loadBinary(name: String, module: Boolean, loader: ClassLoader): Any = + if(module) + getObject(name, loader) + else + { + val clazz = Class.forName(name, true, loader) + clazz.newInstance + } + + def check(discovered: Seq[String], allowMultiple: Boolean): Seq[String] = discovered match { case Seq() => error("No project found") - case Seq(x) => doLoad(x) - case xs => error("Multiple projects found: " + discovered.mkString(", ")) + case Seq(x) => discovered + case _ => if(allowMultiple) discovered else error("Multiple projects found: " + discovered.mkString(", ")) } def error(msg: String) = throw new BuildException(msg) diff --git a/main/build/LoadCommand.scala b/main/build/LoadCommand.scala index f1824fb31..d4b36b377 100644 --- a/main/build/LoadCommand.scala +++ b/main/build/LoadCommand.scala @@ -9,7 +9,7 @@ import java.io.File sealed trait LoadCommand final case class BinaryLoad(classpath: Seq[File], module: Boolean, name: String) extends LoadCommand final case class SourceLoad(classpath: Seq[File], sourcepath: Seq[File], output: Option[File], module: Boolean, auto: Auto.Value, name: String) extends LoadCommand -final case class ProjectLoad(base: File, name: String) extends LoadCommand +final case class ProjectLoad(base: File, auto: Auto.Value, name: String) extends LoadCommand object Auto extends Enumeration { diff --git a/main/build/Parse.scala b/main/build/Parse.scala index 32199b0e7..c4d88ea38 100644 --- a/main/build/Parse.scala +++ b/main/build/Parse.scala @@ -8,48 +8,56 @@ import java.io.File final class ParseException(msg: String) extends RuntimeException(msg) -/** Parses a load command. -* -* load ::= 'load' (binary | source | project) -* -* binary ::= classpath module name -* source ::= classpath '-src' paths ('-d' dir)? ('-auto' ('sub' | 'annot'))? module name -* project ::= ('-project' path)? name? -* -* name ::= '-name' nameString -* module ::= ('-module' ('true'|'false') )? -* classpath ::= '-cp' paths -* path ::= pathChar+ -* paths ::= path (pathSep path)* -*/ - +/** Parses a load command. The implementation is a quick hack. +It is not robust and feedback is not helpful.*/ object Parse { + def helpBrief(name: String, label: String): (String, String) = (name + " ", "Loads " + label + " according to the specified options.") + def helpDetail(name: String, label: String, multiple: Boolean) = +"Loads " + label + """ by one of the following methods: + 1) binary: loads a class from an existing classpath + 2) source: compiles provided sources and loads a specific class + 3) project: builds from source using default settings + +The command has the following syntax: + + load ::= """ + name + """ ('-help' | binary | source | project) + + binary ::= classpath module? name + source ::= classpath? sources out? module? (detect | name) + project ::= base? name? + + base ::= '-project' path + sources ::= '-src' paths + out ::= '-d' dir + detect ::= '-auto' ('sub' | 'annot') + name ::= '-name' nameString + module ::= '-module' ('true'|'false') + classpath ::= '-cp' paths + path ::= pathChar+ + paths ::= path (pathSep path)* +""" + +( if(multiple) "\nTo specify multiple names, separate them by commas." else "") + import File.{pathSeparatorChar => sep} def error(msg: String) = throw new ParseException(msg) def apply(commandString: String)(implicit base: File): LoadCommand = { - val tokens = commandString.split("""\s+""").toSeq - if(tokens.isEmpty) error("Empty command") - else if(tokens.head != "load") error("Not a load command") + val args = commandString.split("""\s+""").toSeq + val srcs = sourcepath(args) + val nme = name(args) + + lazy val cp = classpath(args) + lazy val mod = module(args) + lazy val proj = project(args).getOrElse(base) + + if(!srcs.isEmpty) + SourceLoad(cp, srcs, output(args), mod, auto(args), nme) + else if(!cp.isEmpty) + BinaryLoad(cp, mod, nme) else - { - val args = tokens.drop(1) - val srcs = sourcepath(args) - val nme = name(args) - - lazy val cp = classpath(args) - lazy val mod = module(args) - lazy val proj = project(args).getOrElse(base) - - if(!srcs.isEmpty) - SourceLoad(cp, srcs, output(args), mod, auto(args), nme) - else if(!cp.isEmpty) - BinaryLoad(cp, mod, nme) - else - ProjectLoad(proj, nme) - } + ProjectLoad(proj, auto(args), nme) } def auto(args: Seq[String]): Auto.Value = diff --git a/project/build/XSbt.scala b/project/build/XSbt.scala index 3f314627c..82ad988b3 100644 --- a/project/build/XSbt.scala +++ b/project/build/XSbt.scala @@ -17,7 +17,7 @@ class XSbt(info: ProjectInfo) extends ParentProject(info) with NoCrossPaths val collectionSub = testedBase(utilPath / "collection", "Collections") val ioSub = testedBase(utilPath / "io", "IO", controlSub) val classpathSub = baseProject(utilPath / "classpath", "Classpath", launchInterfaceSub, ioSub) - val completeSub = testedBase(utilPath / "complete", "Completion", ioSub) + val completeSub = project(utilPath / "complete", "Completion", new InputProject(_), controlSub, ioSub) val logSub = project(utilPath / "log", "Logging", new LogProject(_), interfaceSub) val classfileSub = testedBase(utilPath / "classfile", "Classfile", ioSub, interfaceSub, logSub) val datatypeSub = baseProject(utilPath /"datatype", "Datatype Generator", ioSub) @@ -102,6 +102,10 @@ class XSbt(info: ProjectInfo) extends ParentProject(info) with NoCrossPaths override def deliverProjectDependencies = Nil } } + class InputProject(info: ProjectInfo) extends TestedBase(info) + { + val jline = jlineDep + } class WebAppProject(info: ProjectInfo) extends Base(info) { val jetty = "org.mortbay.jetty" % "jetty" % "6.1.14" % "optional" diff --git a/util/complete/LineReader.scala b/util/complete/LineReader.scala new file mode 100644 index 000000000..e7c9ec67a --- /dev/null +++ b/util/complete/LineReader.scala @@ -0,0 +1,86 @@ +/* sbt -- Simple Build Tool + * Copyright 2008, 2009 Mark Harrah + */ +package sbt +import jline.{Completor, ConsoleReader} +abstract class JLine extends LineReader +{ + protected[this] val reader: ConsoleReader + def readLine(prompt: String) = JLine.withJLine { unsynchronizedReadLine(prompt) } + private[this] def unsynchronizedReadLine(prompt: String) = + reader.readLine(prompt) match + { + case null => None + case x => Some(x.trim) + } +} +private object JLine +{ + def terminal = jline.Terminal.getTerminal + def resetTerminal() = withTerminal { _ => jline.Terminal.resetTerminal } + private def withTerminal[T](f: jline.Terminal => T): T = + synchronized + { + val t = terminal + t.synchronized { f(t) } + } + def createReader() = + withTerminal { t => + val cr = new ConsoleReader + t.enableEcho() + cr.setBellEnabled(false) + cr + } + def withJLine[T](action: => T): T = + withTerminal { t => + t.disableEcho() + try { action } + finally { t.enableEcho() } + } + private[sbt] def initializeHistory(cr: ConsoleReader, historyPath: Option[Path]): Unit = + for(historyLocation <- historyPath) + { + val historyFile = historyLocation.asFile + ErrorHandling.wideConvert + { + historyFile.getParentFile.mkdirs() + val history = cr.getHistory + history.setMaxSize(MaxHistorySize) + history.setHistoryFile(historyFile) + } + } + def simple(historyPath: Option[Path]): SimpleReader = new SimpleReader(historyPath) + val MaxHistorySize = 500 +} + +trait LineReader extends NotNull +{ + def readLine(prompt: String): Option[String] +} +private[sbt] final class LazyJLineReader(historyPath: Option[Path], completor: => Completor) extends JLine +{ + protected[this] val reader = + { + val cr = new ConsoleReader + cr.setBellEnabled(false) + JLine.initializeHistory(cr, historyPath) + cr.addCompletor(new LazyCompletor(completor)) + cr + } +} +private class LazyCompletor(delegate0: => Completor) extends Completor +{ + private lazy val delegate = delegate0 + def complete(buffer: String, cursor: Int, candidates: java.util.List[_]): Int = + delegate.complete(buffer, cursor, candidates) +} + +class SimpleReader private[sbt] (historyPath: Option[Path]) extends JLine +{ + protected[this] val reader = JLine.createReader() + JLine.initializeHistory(reader, historyPath) +} +object SimpleReader extends JLine +{ + protected[this] val reader = JLine.createReader() +} \ No newline at end of file