From 98c98f9c2689cd2ee6bf7e9b8e116eb83cccf73b Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Sun, 29 Jan 2012 14:36:27 -0500 Subject: [PATCH] split command core to main/command/ --- main/Act.scala | 2 +- ...mandSupport.scala => CommandStrings.scala} | 202 ++-------- main/IvyConsole.scala | 2 +- main/Keys.scala | 7 +- main/Load.scala | 4 +- main/LogManager.scala | 38 +- main/Main.scala | 370 ++---------------- main/Project.scala | 2 +- main/Script.scala | 2 +- main/TaskData.scala | 2 +- main/command/BasicCommandStrings.scala | 126 ++++++ main/command/BasicCommands.scala | 223 +++++++++++ main/command/BasicKeys.scala | 10 + main/{ => command}/Command.scala | 0 main/command/CommandUtil.scala | 32 ++ main/{ => command}/MainControl.scala | 0 main/command/MainLoop.scala | 100 +++++ main/{ => command}/State.scala | 6 +- main/{ => command}/Watched.scala | 5 +- project/Sbt.scala | 6 +- sbt/package.scala | 3 + tasks/Incomplete.scala | 2 +- util/collection/Settings.scala | 2 +- util/control/MessageOnlyException.scala | 9 +- util/log/GlobalLogging.scala | 27 ++ util/log/MainLogging.scala | 36 ++ 26 files changed, 662 insertions(+), 556 deletions(-) rename main/{CommandSupport.scala => CommandStrings.scala} (51%) create mode 100644 main/command/BasicCommandStrings.scala create mode 100644 main/command/BasicCommands.scala create mode 100644 main/command/BasicKeys.scala rename main/{ => command}/Command.scala (100%) create mode 100644 main/command/CommandUtil.scala rename main/{ => command}/MainControl.scala (100%) create mode 100644 main/command/MainLoop.scala rename main/{ => command}/State.scala (99%) rename main/{ => command}/Watched.scala (96%) create mode 100644 util/log/GlobalLogging.scala create mode 100644 util/log/MainLogging.scala diff --git a/main/Act.scala b/main/Act.scala index 39c40c135..7d7d98610 100644 --- a/main/Act.scala +++ b/main/Act.scala @@ -11,7 +11,7 @@ package sbt import DefaultParsers._ import Types.idFun import java.net.URI - import CommandSupport.ShowCommand + import CommandStrings.ShowCommand final class ParsedKey(val key: ScopedKey[_], val mask: ScopeMask) object Act diff --git a/main/CommandSupport.scala b/main/CommandStrings.scala similarity index 51% rename from main/CommandSupport.scala rename to main/CommandStrings.scala index b65ae5f43..7dc1a62d9 100644 --- a/main/CommandSupport.scala +++ b/main/CommandStrings.scala @@ -3,37 +3,15 @@ */ package sbt -import complete.HistoryCommands -import scala.annotation.tailrec - -import java.io.File -import Path._ - -object CommandSupport +object CommandStrings { - def logger(s: State) = globalLogging(s).full - def globalLogging(s: State) = s get Keys.globalLogging getOrElse error("Global logging misconfigured") + @deprecated("Use the `log` member of a State instance directly.", "0.12.0") + def logger(s: State) = s.log - // slightly better fallback in case of older launcher - def bootDirectory(state: State): File = - try { state.configuration.provider.scalaProvider.launcher.bootDirectory } - catch { case e: NoSuchMethodError => new File(".").getAbsoluteFile } - - private def canRead = (_: File).canRead - def notReadable(files: Seq[File]): Seq[File] = files filterNot 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("#") + @deprecated("Use the `globalLogging` member of a State instance directly.", "0.12.0") + def globalLogging(s: State) = s.globalLogging /** The prefix used to identify a request to execute the remaining input on source changes.*/ - val ContinuousExecutePrefix = "~" - val HelpCommand = "help" val AboutCommand = "about" val TasksCommand = "tasks" val ProjectCommand = "project" @@ -41,8 +19,14 @@ object CommandSupport val ShowCommand = "show" val BootCommand = "boot" - val Exit = "exit" - val Quit = "quit" + @deprecated("Moved to BasicCommandStrings", "0.12.0") + val ContinuousExecutePrefix = BasicCommandStrings.ContinuousExecutePrefix + + @deprecated("Moved to BasicCommandStrings", "0.12.0") + val Exit = BasicCommandStrings.Exit + + @deprecated("Moved to BasicCommandStrings", "0.12.0") + val Quit = BasicCommandStrings.Quit val EvalCommand = "eval" val evalBrief = (EvalCommand + " ", "Evaluates the given Scala expression and prints the result and type.") @@ -128,9 +112,8 @@ SetCommand + """ def sessionBrief = (SessionCommand + " ", "Manipulates session settings. For details, run 'help " + SessionCommand + "'.") /** The command name to terminate the program.*/ - val TerminateAction: String = Exit - - def continuousBriefHelp = (ContinuousExecutePrefix + " ", "Executes the specified command whenever source files change.") + @deprecated("Moved to BasicCommandStrings", "0.12.0") + val TerminateAction: String = BasicCommandStrings.TerminateAction def tasksPreamble = """ This is a list of tasks defined for the current project. @@ -140,11 +123,6 @@ Tasks produce values. Use the 'show' command to run the task and print the resu def tasksBrief = "Displays the tasks defined for the current project." def tasksDetailed = "Displays the tasks defined directly or indirectly for the current project." - 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. -Otherwise, this prints a help summary.""" - def aboutBrief = "Displays basic information about sbt and the build." def aboutDetailed = aboutBrief @@ -175,126 +153,23 @@ ProjectCommand + def projectsBrief = projectsDetailed def projectsDetailed = "Displays the names of available projects." - def historyHelp = Help.briefDetail(HistoryCommands.descriptions) - - def exitBrief = "Terminates the build." - def sbtrc = ".sbtrc" - def ReadCommand = "<" - def ReadFiles = " file1 file2 ..." - def ReadBrief = (ReadCommand + " *", "Reads command lines from the provided files.") - def ReadDetailed = -ReadCommand + ReadFiles + """ - - Reads the lines from the given files and inserts them as commands. - All empty lines and lines that start with '#' are ignored. - If a file does not exist or is not readable, this command fails. - - All the lines from all the files are read before any of the commands - are executed. Thus, if any file is not readable, none of commands - from any of the files (even the existing ones) will be run. - - You probably need to escape this command if entering it at your shell.""" - - def ApplyCommand = "apply" - def ApplyBrief = (ApplyCommand + " *", ApplyDetailed) - def ApplyDetailed = "Transforms the current State by calling .apply(currentState) for each listed." + @deprecated("Moved to BasicCommandStrings", "0.12.0") + def ReadCommand = BasicCommandStrings.ReadCommand def DefaultsCommand = "add-default-commands" def DefaultsBrief = (DefaultsCommand, DefaultsDetailed) def DefaultsDetailed = "Registers default built-in commands" + @deprecated("Moved to BasicCommandStrings", "0.12.0") def RebootCommand = "reboot" - def RebootSummary = RebootCommand + " [full]" - def RebootBrief = (RebootSummary, "Reboots sbt and then executes the remaining commands.") - def RebootDetailed = -RebootSummary + """ - - This command is equivalent to exiting sbt, restarting, and running the - remaining commands with the exception that the JVM is not shut down. - - If 'full' is specified, the boot directory (`~/.sbt/boot` by default) - is deleted before restarting. This forces an update of sbt and Scala - and is useful when working with development versions of sbt or Scala.""" + @deprecated("Moved to BasicCommandStrings", "0.12.0") def Multi = ";" - def MultiBrief = (Multi + " (" + Multi + " )*", "Runs the provided semicolon-separated commands.") - def MultiDetailed = -Multi + " command1 " + Multi + """ command2 ... - - Runs the specified commands.""" + @deprecated("Moved to BasicCommandStrings", "0.12.0") def AppendCommand = "append" - def AppendLastBrief = (AppendCommand + " ", AppendLastDetailed) - def AppendLastDetailed = "Appends 'command' to list of commands to run." - - 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, the corresponding `value` is run. - If any argument is provided to `name`, it is appended as argument to `value`. - -""" + -AliasCommand + """ name= - - Removes the alias for `name`.""" - - def Discover = "discover" - def DiscoverBrief = (DiscoverSyntax, "Finds annotated classes and subclasses.") - def DiscoverSyntax = Discover + " [-module true|false] [-sub ] [-annot ]" - def DiscoverDetailed = -DiscoverSyntax + """ - - Looks for public, concrete classes that match the requested query using the current sbt.inc.Analysis instance. - - -module - Specifies whether modules (true) or classes (false) are found. - The default is classes/traits (false). - - -sub - Specifies comma-separated class names. - Classes that have one or more of these classes as an ancestor are included in the resulting list. - - -annot - Specifies comma-separated annotation names. - Classes with one or more of these annotations on the class or one of its non-private methods are included in the resulting list. -""" - - def CompileName = "direct-compile" - def CompileBrief = (CompileSyntax, "Incrementally compiles the provided sources.") - def CompileSyntax = CompileName + " -src [-cp ] [-d ]" - def CompileDetailed = -CompileSyntax + """ - - Incrementally compiles Scala and Java sources. - - are explicit paths separated by the platform path separator. - - The specified output path will contain the following directory structure: - - scala_/ - classes/ - cache/ - - Compiled classes will be written to the 'classes' directory. - Cached information about the compilation will be written to 'cache'. -""" - - val FailureWall = "---" def Load = "load" def LoadLabel = "a project" @@ -308,31 +183,20 @@ CompileSyntax + """ def LoadProjectBrief = (LoadProject, LoadProjectDetailed) def LoadProjectDetailed = "Loads the project in the current directory" - def Shell = "shell" - def ShellBrief = ShellDetailed - def ShellDetailed = "Provides an interactive prompt from which commands can be run." + @deprecated("Moved to State", "0.12.0") + val FailureWall = State.FailureWall + @deprecated("Moved to BasicCommandStrings", "0.12.0") + def Shell = BasicCommandStrings.Shell + + @deprecated("Moved to BasicCommandStrings", "0.12.0") def ClearOnFailure = "--" + + @deprecated("Moved to BasicCommandStrings", "0.12.0") 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 resets when it runs once, so it must be added - again if desired.""" + @deprecated("Moved to BasicCommandStrings", "0.12.0") def IfLast = "iflast" - def IfLastBrief = (IfLast + " ", IfLastCommon) - def IfLastCommon = "If there are no more commands after this one, 'command' is run." - def IfLastDetailed = -IfLast + """ command - - """ + IfLastCommon def InitCommand = "initialize" def InitBrief = (InitCommand, "Initializes command processing.") @@ -352,4 +216,12 @@ load-commands -base ~/.sbt/commands < .sbtrc Runs commands from ~/.sbtrc and ./.sbtrc if they exist """ + + import java.io.File + import Path._ + + def sbtRCs(s: State): Seq[File] = + (Path.userHome / sbtrc) :: + (s.baseDir / sbtrc asFile) :: + Nil } diff --git a/main/IvyConsole.scala b/main/IvyConsole.scala index 9cdf09725..39318f851 100644 --- a/main/IvyConsole.scala +++ b/main/IvyConsole.scala @@ -14,7 +14,7 @@ object IvyConsole lazy val command = Command.command(Name) { state => val Dependencies(managed, repos, unmanaged) = parseDependencies(state.remainingCommands, state.log) - val base = new File(CommandSupport.bootDirectory(state), Name) + val base = new File(CommandUtil.bootDirectory(state), Name) IO.createDirectory(base) val (eval, structure) = Load.defaultLoad(state, base, state.log) diff --git a/main/Keys.scala b/main/Keys.scala index ae875d36b..3881f99e2 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -56,11 +56,10 @@ object Keys // val onComplete = SettingKey[RMap[Task,Result] => RMap[Task,Result]]("on-complete", "Transformation to apply to the final task result map. This may also be used to register hooks to run when task evaluation completes.") // Command keys - val globalLogging = AttributeKey[GlobalLogging]("global-logging", "Provides a global Logger, including command logging.") - val historyPath = SettingKey[Option[File]]("history", "The location where command line history is persisted.") - val shellPrompt = SettingKey[State => String]("shell-prompt", "The function that constructs the command prompt from the current build state.") + val historyPath = SettingKey(BasicKeys.historyPath) + val shellPrompt = SettingKey(BasicKeys.shellPrompt) val analysis = AttributeKey[inc.Analysis]("analysis", "Analysis of compilation, including dependencies and generated outputs.") - val watch = SettingKey[Watched]("watch", "Continuous execution configuration.") + val watch = SettingKey(BasicKeys.watch) val pollInterval = SettingKey[Int]("poll-interval", "Interval between checks for modified sources by the continuous execution command.") val watchSources = TaskKey[Seq[File]]("watch-sources", "Defines the sources in this project for continuous execution to watch for changes.") val watchTransitiveSources = TaskKey[Seq[File]]("watch-transitive-sources", "Defines the sources in all projects for continuous execution to watch.") diff --git a/main/Load.scala b/main/Load.scala index 4a7d7c989..bc6bbc39f 100644 --- a/main/Load.scala +++ b/main/Load.scala @@ -14,7 +14,7 @@ package sbt import inc.{FileValueCache, Locate} import Project.{inScope, ScopedKey, ScopeLocal, Setting} import Keys.{appConfiguration, baseDirectory, configuration, streams, Streams, thisProject, thisProjectRef} - import Keys.{globalLogging, isDummy, loadedBuild, parseResult, resolvedScoped, taskDefinitionKey} + import Keys.{isDummy, loadedBuild, parseResult, resolvedScoped, taskDefinitionKey} import tools.nsc.reporters.ConsoleReporter import Build.{analyzed, data} import Scope.{GlobalScope, ThisScope} @@ -489,7 +489,7 @@ object Load val inputs = Compiler.inputs(data(classpath), sources, target, Nil, Nil, definesClass, Compiler.DefaultMaxErrors, CompileOrder.Mixed)(compilers, log) val analysis = try { Compiler(inputs, log) } - catch { case _: xsbti.CompileFailed => throw new NoMessageException } // compiler already logged errors + catch { case _: xsbti.CompileFailed => throw new AlreadyHandledException } // compiler already logged errors (inputs, analysis) } diff --git a/main/LogManager.scala b/main/LogManager.scala index 28721d5e2..3f82a3601 100644 --- a/main/LogManager.scala +++ b/main/LogManager.scala @@ -9,6 +9,7 @@ package sbt import std.Transform import Project.ScopedKey import Scope.GlobalScope + import MainLogging._ import Keys.{logLevel, logManager, persistLogLevel, persistTraceLevel, state, traceLevel} object LogManager @@ -21,11 +22,6 @@ object LogManager lazy val default: LogManager = withLoggers() def defaults(extra: ScopedKey[_] => Seq[AbstractLogger]): LogManager = withLoggers(extra = extra) - def defaultScreen: AbstractLogger = ConsoleLogger() - - def defaultBacked(useColor: Boolean = ConsoleLogger.formatEnabled): PrintWriter => ConsoleLogger = - to => ConsoleLogger(ConsoleLogger.printWriterOut(to), useColor = useColor) // TODO: should probably filter ANSI codes when useColor=false - def withScreenLogger(mk: => AbstractLogger): LogManager = withLoggers(mk) def withLoggers(screen: => AbstractLogger = defaultScreen, backed: PrintWriter => AbstractLogger = defaultBacked(), extra: ScopedKey[_] => Seq[AbstractLogger] = _ => Nil): LogManager = @@ -42,40 +38,12 @@ object LogManager val backingLevel = getOr(persistLogLevel.key, Level.Debug) val screenTrace = getOr(traceLevel.key, -1) val backingTrace = getOr(persistTraceLevel.key, Int.MaxValue) - val extraBacked = (state get Keys.globalLogging).map(_.backed).toList + val extraBacked = state.globalLogging.backed :: Nil multiLogger( new MultiLoggerConfig(console, backed, extraBacked ::: extra, screenLevel, backingLevel, screenTrace, backingTrace) ) } - def multiLogger(config: MultiLoggerConfig): Logger = - { - import config._ - val multi = new MultiLogger(console :: backed :: extra) - // sets multi to the most verbose for clients that inspect the current level - multi setLevel Level.unionAll(backingLevel :: screenLevel :: extra.map(_.getLevel)) - // set the specific levels - console setLevel screenLevel - backed setLevel backingLevel - console setTrace screenTrace - backed setTrace backingTrace - multi: Logger - } - def globalDefault(writer: PrintWriter, backing: GlobalLogBacking): GlobalLogging = - { - val backed = defaultBacked()(writer) - val full = multiLogger(defaultMultiConfig( backed ) ) - GlobalLogging(full, backed, backing) - } - - def defaultMultiConfig(backing: AbstractLogger): MultiLoggerConfig = - new MultiLoggerConfig(defaultScreen, backing, Nil, Level.Info, Level.Debug, -1, Int.MaxValue) } -final case class MultiLoggerConfig(console: AbstractLogger, backed: AbstractLogger, extra: List[AbstractLogger], screenLevel: Level.Value, backingLevel: Level.Value, screenTrace: Int, backingTrace: Int) + trait LogManager { def apply(data: Settings[Scope], state: State, task: ScopedKey[_], writer: PrintWriter): Logger } -final case class GlobalLogBacking(file: File, last: Option[File]) -{ - def shift(newFile: File) = GlobalLogBacking(newFile, Some(file)) - def unshift = GlobalLogBacking(last getOrElse file, None) -} -final case class GlobalLogging(full: Logger, backed: ConsoleLogger, backing: GlobalLogBacking) diff --git a/main/Main.scala b/main/Main.scala index acb565613..70d67ce5a 100644 --- a/main/Main.scala +++ b/main/Main.scala @@ -3,132 +3,64 @@ */ package sbt - import Execute.NodeView - import complete.{DefaultParsers, HistoryCommands, Parser} - import HistoryCommands.{Start => HistoryPrefix} + import complete.{DefaultParsers, Parser} import compiler.EvalImports - import Types.{const,idFun} + import Types.idFun import Aggregation.AnyKeys - import Command.applyEffect - import Keys.{analysis,historyPath,globalLogging,shellPrompt} import scala.annotation.tailrec - import scala.collection.JavaConversions._ - import Function.tupled - import java.net.URI - import java.lang.reflect.InvocationTargetException import Path._ + import StandardMain._ import java.io.File + import java.net.URI /** This class is the entry point for sbt.*/ final class xMain extends xsbti.AppMain { def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = { - import BuiltinCommands.{initialAttributes, initialize, defaults, DefaultBootCommands} - import CommandSupport.{BootCommand, DefaultsCommand, InitCommand} - val initialCommandDefs = Seq(initialize, defaults) - val commands = DefaultsCommand +: InitCommand +: BootCommand +: configuration.arguments.map(_.trim) - val state = State( configuration, initialCommandDefs, Set.empty, None, commands, State.newHistory, initialAttributes, State.Continue ) - MainLoop.runLogged(state) + import BuiltinCommands.{initialize, defaults} + import CommandStrings.{BootCommand, DefaultsCommand, InitCommand} + MainLoop.runLogged( initialState(configuration, + Seq(initialize, defaults), + DefaultsCommand :: InitCommand :: BootCommand :: Nil) + ) } } final class ScriptMain extends xsbti.AppMain { def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = - { - import BuiltinCommands.{initialAttributes, ScriptCommands} - val commands = Script.Name +: configuration.arguments.map(_.trim) - val state = State( configuration, ScriptCommands, Set.empty, None, commands, State.newHistory, initialAttributes, State.Continue ) - MainLoop.runLogged(state) - } + MainLoop.runLogged( initialState(configuration, + BuiltinCommands.ScriptCommands, + Script.Name :: Nil) + ) } final class ConsoleMain extends xsbti.AppMain { def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = - { - import BuiltinCommands.{initialAttributes, ConsoleCommands} - val commands = IvyConsole.Name +: configuration.arguments.map(_.trim) - val state = State( configuration, ConsoleCommands, Set.empty, None, commands, State.newHistory, initialAttributes, State.Continue ) - MainLoop.runLogged(state) - } + MainLoop.runLogged( initialState(configuration, + BuiltinCommands.ConsoleCommands, + IvyConsole.Name :: Nil) + ) } -object MainLoop + +object StandardMain { - /** Entry point to run the remaining commands in State with managed global logging.*/ - def runLogged(state: State): xsbti.MainResult = - runLoggedLoop(state, GlobalLogBacking(newBackingFile(), None)) - - /** Constructs a new, (weakly) unique, temporary file to use as the backing for global logging. */ - def newBackingFile(): File = File.createTempFile("sbt",".log") - - /** Run loop that evaluates remaining commands and manages changes to global logging configuration.*/ - @tailrec def runLoggedLoop(state: State, logBacking: GlobalLogBacking): xsbti.MainResult = - runAndClearLast(state, logBacking) match { - case ret: Return => // delete current and last log files when exiting normally - logBacking.file.delete() - deleteLastLog(logBacking) - ret.result - case clear: ClearGlobalLog => // delete previous log file, move current to previous, and start writing to a new file - deleteLastLog(logBacking) - runLoggedLoop(clear.state, logBacking shift newBackingFile()) - case keep: KeepGlobalLog => // make previous log file the current log file - logBacking.file.delete - runLoggedLoop(keep.state, logBacking.unshift) - } - - /** Runs the next sequence of commands, cleaning up global logging after any exceptions. */ - def runAndClearLast(state: State, logBacking: GlobalLogBacking): RunNext = - try - runWithNewLog(state, logBacking) - catch { - case e: xsbti.FullReload => - deleteLastLog(logBacking) - throw e // pass along a reboot request - case e => - System.err.println("sbt appears to be exiting abnormally.\n The log file for this session is at " + logBacking.file) - deleteLastLog(logBacking) - throw e - } - - /** Deletes the previous global log file. */ - def deleteLastLog(logBacking: GlobalLogBacking): Unit = - logBacking.last.foreach(_.delete()) - - /** Runs the next sequence of commands with global logging in place. */ - def runWithNewLog(state: State, logBacking: GlobalLogBacking): RunNext = - Using.fileWriter(append = true)(logBacking.file) { writer => - val out = new java.io.PrintWriter(writer) - val loggedState = state.put(globalLogging, LogManager.globalDefault(out, logBacking)) - try run(loggedState) finally out.close() - } - sealed trait RunNext - final class ClearGlobalLog(val state: State) extends RunNext - final class KeepGlobalLog(val state: State) extends RunNext - final class Return(val result: xsbti.MainResult) extends RunNext - - /** Runs the next sequence of commands that doesn't require global logging changes.*/ - @tailrec def run(state: State): RunNext = - state.next match - { - case State.Continue => run(next(state)) - case State.ClearGlobalLog => new ClearGlobalLog(state.continue) - case State.KeepLastLog => new KeepGlobalLog(state.continue) - case ret: State.Return => new Return(ret.result) - } - - def next(state: State): State = - ErrorHandling.wideConvert { state.process(Command.process) } match - { - case Right(s) => s - case Left(t: xsbti.FullReload) => throw t - case Left(t) => BuiltinCommands.handleException(t, state) - } + def initialState(configuration: xsbti.AppConfiguration, initialDefinitions: Seq[Command], preCommands: Seq[String]): State = + { + val commands = preCommands ++ configuration.arguments.map(_.trim) + State( configuration, initialDefinitions, Set.empty, None, commands, State.newHistory, BuiltinCommands.initialAttributes, initialGlobalLogging, State.Continue ) + } + def initialGlobalLogging: GlobalLogging = + GlobalLogging.initial(MainLogging.globalDefault _, File.createTempFile("sbt",".log")) } import DefaultParsers._ - import CommandSupport._ + import CommandStrings._ + import BasicCommands._ + import CommandUtil._ + object BuiltinCommands { def initialAttributes = AttributeMap.empty @@ -140,22 +72,9 @@ object BuiltinCommands def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil def boot = Command.make(BootCommand)(bootParser) - def nop = Command.custom(s => success(() => s)) - def ignore = Command.command(FailureWall)(idFun) - def detail(selected: Seq[String], detailMap: Map[String, String]): Seq[String] = - selected.distinct flatMap { detailMap get _ } - - def help = Command.make(HelpCommand, helpBrief, helpDetailed)(helpParser) def about = Command.command(AboutCommand, aboutBrief, aboutDetailed) { s => logger(s).info(aboutString(s)); s } - def helpParser(s: State) = - { - val h = (Help.empty /: s.definedCommands)(_ ++ _.help(s)) - val helpCommands = h.detail.keySet - val args = (token(Space) ~> token( NotSpace examples helpCommands )).* - applyEffect(args)(runHelp(s, h)) - } // This parser schedules the default boot commands unless overridden by an alias def bootParser(s: State) = { @@ -163,16 +82,6 @@ object BuiltinCommands delegateToAlias(BootCommand, success(orElse) )(s) } - def runHelp(s: State, h: Help)(args: Seq[String]): State = - { - val message = - if(args.isEmpty) - aligned(" ", " ", h.brief).mkString("\n", "\n", "\n") - else - detail(args, h.detail) mkString("\n", "\n\n", "\n") - System.out.println(message) - s - } def sbtVersion(s: State): String = s.configuration.provider.id.version def scalaVersion(s: State): String = s.configuration.provider.scalaProvider.version def aboutString(s: State): String = @@ -230,154 +139,15 @@ object BuiltinCommands aligned(" ", " ", taskDetail(s)) mkString("\n", "\n", "") def taskStrings(key: AttributeKey[_]): Option[(String, String)] = key.description map { d => (key.label, d) } - def aligned(pre: String, sep: String, in: Seq[(String, String)]): Seq[String] = - { - val width = in.map(_._1.length).max - in.map { case (a, b) => (" " + fill(a, width) + sep + b) } - } - def fill(s: String, size: Int) = s + " " * math.max(size - s.length, 0) - - def alias = Command.make(AliasCommand, AliasBrief, AliasDetailed) { s => - val name = token(OpOrID.examples( aliasNames(s) : _*) ) - val assign = token(OptSpace ~ '=' ~ OptSpace) - val sfree = removeAliases(s) - val to = matched(sfree.combinedParser, partial = true) | any.+.string - val base = (OptSpace ~> (name ~ (assign ~> to.?).?).?) - applyEffect(base)(t => runAlias(s, t) ) - } - - def runAlias(s: State, args: Option[(String, Option[Option[String]])]): State = - args match - { - case None => printAliases(s); s - case Some(x ~ None) if !x.isEmpty => printAlias(s, x.trim); s - case Some(name ~ Some(None)) => removeAlias(s, name.trim) - case Some(name ~ Some(Some(value))) => addAlias(s, name.trim, value.trim) - } - - def shell = Command.command(Shell, ShellBrief, ShellDetailed) { s => - val history = (s get historyPath.key) getOrElse Some((s.baseDir / ".history").asFile) - val prompt = (s get shellPrompt.key) match { case Some(pf) => pf(s); case None => "> " } - val reader = new FullReader(history, s.combinedParser) - val line = reader.readLine(prompt) - line match { - case Some(line) => - val newState = s.copy(onFailure = Some(Shell), remainingCommands = line +: Shell +: s.remainingCommands) - if(line.trim.isEmpty) newState else newState.clearGlobalLog - case None => s - } - } - - def multiParser(s: State): Parser[Seq[String]] = - { - val nonSemi = token(charClass(_ != ';').+, hide= const(true)) - ( token(';' ~> OptSpace) flatMap { _ => matched((s.combinedParser&nonSemi) | nonSemi) <~ token(OptSpace) } map (_.trim) ).+ - } - - def multiApplied(s: State) = - Command.applyEffect( multiParser(s) )( _ ::: s ) - - def multi = Command.custom(multiApplied, Help(Multi, MultiBrief, MultiDetailed) ) - - lazy val otherCommandParser = (s: State) => token(OptSpace ~> combinedLax(s, any.+) ) - def combinedLax(s: State, any: Parser[_]): Parser[String] = - matched(s.combinedParser | token(any, hide= const(true))) - - def ifLast = Command(IfLast, IfLastBrief, IfLastDetailed)(otherCommandParser) { (s, arg) => - if(s.remainingCommands.isEmpty) arg :: s else s - } - def append = Command(AppendCommand, AppendLastBrief, AppendLastDetailed)(otherCommandParser) { (s, arg) => - s.copy(remainingCommands = s.remainingCommands :+ arg) - } - - def setOnFailure = Command(OnFailure, OnFailureBrief, OnFailureDetailed)(otherCommandParser) { (s, arg) => - s.copy(onFailure = Some(arg)) - } - def clearOnFailure = Command.command(ClearOnFailure)(s => s.copy(onFailure = None)) - - def reboot = Command(RebootCommand, RebootBrief, RebootDetailed)(rebootParser) { (s, full) => - s.reboot(full) - } - def rebootParser(s: State) = token(Space ~> "full" ^^^ true) ?? false def defaults = Command.command(DefaultsCommand) { s => s ++ DefaultCommands } - def call = Command(ApplyCommand, ApplyBrief, ApplyDetailed)(_ => spaceDelimited("")) { (state,args) => - val loader = getClass.getClassLoader - val loaded = args.map(arg => ModuleUtilities.getObject(arg, loader)) - (state /: loaded) { case (s, obj: (State => State)) => obj(s) } - } def initialize = Command.command(InitCommand) { s => /*"load-commands -base ~/.sbt/commands" :: */readLines( readable( sbtRCs(s) ) ) ::: s } - def readParser(s: State) = - { - val files = (token(Space) ~> fileParser(s.baseDir)).+ - val portAndSuccess = token(OptSpace) ~> Port - portAndSuccess || files - } - - def read = Command.make(ReadCommand, ReadBrief, ReadDetailed)(s => applyEffect(readParser(s))(doRead(s)) ) - - def doRead(s: State)(arg: Either[Int, Seq[File]]): State = - arg match - { - case Left(portAndSuccess) => - val port = math.abs(portAndSuccess) - val previousSuccess = portAndSuccess >= 0 - readMessage(port, previousSuccess) match - { - case Some(message) => (message :: (ReadCommand + " " + port) :: s).copy(onFailure = Some(ReadCommand + " " + (-port))) - case None => - System.err.println("Connection closed.") - s.fail - } - case Right(from) => - 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("Command file(s) not readable: \n\t" + notFound.mkString("\n\t")) - s - } - } - private def readMessage(port: Int, previousSuccess: Boolean): Option[String] = - { - // split into two connections because this first connection ends the previous communication - xsbt.IPC.client(port) { _.send(previousSuccess.toString) } - // and this second connection starts the next communication - xsbt.IPC.client(port) { ipc => - val message = ipc.receive - if(message eq null) None else Some(message) - } - } - - def continuous = - Command(ContinuousExecutePrefix, Help(continuousBriefHelp) )(otherCommandParser) { (s, arg) => - withAttribute(s, Watched.Configuration, "Continuous execution not configured.") { w => - val repeat = ContinuousExecutePrefix + (if(arg.startsWith(" ")) arg else " " + arg) - Watched.executeContinuously(w, s, arg, repeat) - } - } - - def history = Command.custom(historyParser, historyHelp) - def historyParser(s: State): Parser[() => State] = - Command.applyEffect(HistoryCommands.actionParser) { histFun => - val logError = (msg: String) => s.log.error(msg) - val hp = s get historyPath.key getOrElse None - val lines = hp.toList.flatMap( p => IO.readLines(p) ).toIndexedSeq - histFun( complete.History(lines, hp, logError) ) match - { - case Some(commands) => - commands foreach println //printing is more appropriate than logging - (commands ::: s).continue - case None => s.fail - } - } - def eval = Command.single(EvalCommand, evalBrief, evalDetailed) { (s, arg) => val log = logger(s) val extracted = Project extract s @@ -482,7 +252,7 @@ object BuiltinCommands /** Determines the log file that last* commands should operate on. See also isLastOnly. */ def lastLogFile(s: State) = { - val backing = CommandSupport.globalLogging(s).backing + val backing = s.globalLogging.backing if(isLastOnly(s)) backing.last else Some(backing.file) } @@ -514,7 +284,7 @@ object BuiltinCommands } def act = Command.customHelp(Act.actParser, actHelp) - def actHelp = (s: State) => CommandSupport.showHelp ++ keysHelp(s) + def actHelp = (s: State) => CommandStrings.showHelp ++ keysHelp(s) def keysHelp(s: State): Help = if(Project.isProjectLoaded(s)) Help.detailOnly(taskDetail(s)) @@ -530,16 +300,9 @@ object BuiltinCommands for( (uri, build) <- structure.units if curi != uri) listBuild(uri, build, false, cid, log) s } - def withAttribute[T](s: State, key: AttributeKey[T], ifMissing: String)(f: T => State): State = - (s get key) match { - case None => logger(s).error(ifMissing); s.fail - case Some(nav) => f(nav) - } def project = Command.make(ProjectCommand, projectBrief, projectDetailed)(ProjectNavigation.command) - def exit = Command.command(TerminateAction, exitBrief, exitBrief ) ( _ exit true ) - def loadFailed = Command.command(LoadFailed)(handleLoadFailed) @tailrec def handleLoadFailed(s: State): State = { @@ -576,71 +339,4 @@ object BuiltinCommands SessionSettings.checkSession(session, s) Project.setProject(session, structure, s) } - - def handleException(e: Throwable, s: State): State = - handleException(e, s, logger(s)) - def handleException(e: Throwable, s: State, log: Logger): State = - { - e match - { - case _: Incomplete => () // already handled by evaluateTask - case _: NoMessageException => () - case ite: InvocationTargetException => - val cause = ite.getCause - if(cause == null || cause == ite) logFullException(ite, log) else handleException(cause, s, log) - case _: MessageOnlyException => log.error(e.toString) - case _: Project.Uninitialized => logFullException(e, log, true) - case _ => logFullException(e, log) - } - s.fail - } - def logFullException(e: Throwable, log: Logger, messageOnly: Boolean = false) - { - log.trace(e) - log.error(if(messageOnly) e.getMessage else ErrorHandling reducedToString e) - log.error("Use 'last' for the full log.") - } - - def addAlias(s: State, name: String, value: String): State = - if(Command validID name) { - val removed = removeAlias(s, name) - if(value.isEmpty) removed else removed.copy(definedCommands = newAlias(name, value) +: removed.definedCommands) - } else { - System.err.println("Invalid alias name '" + name + "'.") - s.fail - } - - def removeAliases(s: State): State = removeTagged(s, CommandAliasKey) - def removeAlias(s: State, name: String): State = s.copy(definedCommands = s.definedCommands.filter(c => !isAliasNamed(name, c)) ) - - def removeTagged(s: State, tag: AttributeKey[_]): State = s.copy(definedCommands = removeTagged(s.definedCommands, tag)) - def removeTagged(as: Seq[Command], tag: AttributeKey[_]): Seq[Command] = as.filter(c => ! (c.tags contains tag)) - - def isAliasNamed(name: String, c: Command): Boolean = isNamed(name, getAlias(c)) - def isNamed(name: String, alias: Option[(String,String)]): Boolean = alias match { case None => false; case Some((n,_)) => name == n } - - def getAlias(c: Command): Option[(String,String)] = c.tags get CommandAliasKey - def printAlias(s: State, name: String): Unit = printAliases(aliases(s,(n,v) => n == name) ) - def printAliases(s: State): Unit = printAliases(allAliases(s)) - def printAliases(as: Seq[(String,String)]): Unit = - for( (name,value) <- as) - println("\t" + name + " = " + value) - - def aliasNames(s: State): Seq[String] = allAliases(s).map(_._1) - def allAliases(s: State): Seq[(String,String)] = aliases(s, (n,v) => true) - def aliases(s: State, pred: (String,String) => Boolean): Seq[(String,String)] = - s.definedCommands.flatMap(c => getAlias(c).filter(tupled(pred))) - - def newAlias(name: String, value: String): Command = - Command.make(name, (name, "'" + value + "'"), "Alias of '" + value + "'")(aliasBody(name, value)).tag(CommandAliasKey, (name, value)) - def aliasBody(name: String, value: String)(state: State): Parser[() => State] = - OptSpace ~> Parser(Command.combine(removeAlias(state,name).definedCommands)(state))(value) - - def delegateToAlias(name: String, orElse: Parser[() => State])(state: State): Parser[() => State] = - aliases(state, (nme,_) => nme == name).headOption match { - case None => orElse - case Some((n,v)) => aliasBody(n,v)(state) - } - - val CommandAliasKey = AttributeKey[(String,String)]("is-command-alias", "Internal: marker for Commands created as aliases for another command.") } \ No newline at end of file diff --git a/main/Project.scala b/main/Project.scala index b87de657f..80bb4e4c2 100755 --- a/main/Project.scala +++ b/main/Project.scala @@ -203,7 +203,7 @@ object Project extends Init[Scope] with ProjectExtra val prompt = get(shellPrompt) val watched = get(watch) val commandDefs = allCommands.distinct.flatten[Command].map(_ tag (projectCommand, true)) - val newDefinedCommands = commandDefs ++ BuiltinCommands.removeTagged(s.definedCommands, projectCommand) + val newDefinedCommands = commandDefs ++ BasicCommands.removeTagged(s.definedCommands, projectCommand) val newAttrs = setCond(Watched.Configuration, watched, s.attributes).put(historyPath.key, history) s.copy(attributes = setCond(shellPrompt.key, prompt, newAttrs), definedCommands = newDefinedCommands) } diff --git a/main/Script.scala b/main/Script.scala index 2be888410..e315f623d 100644 --- a/main/Script.scala +++ b/main/Script.scala @@ -16,7 +16,7 @@ object Script val scriptArg = state.remainingCommands.headOption getOrElse error("No script file specified") val script = new File(scriptArg).getAbsoluteFile val hash = Hash.halve(Hash.toHex(Hash(script.getAbsolutePath))) - val base = new File(CommandSupport.bootDirectory(state), hash) + val base = new File(CommandUtil.bootDirectory(state), hash) IO.createDirectory(base) val (eval, structure) = Load.defaultLoad(state, base, state.log) diff --git a/main/TaskData.scala b/main/TaskData.scala index 95286f5f8..999af6f7c 100644 --- a/main/TaskData.scala +++ b/main/TaskData.scala @@ -47,6 +47,6 @@ object TaskData private[this] def fakeState(structure: BuildStructure): State = { val config = Keys.appConfiguration in Scope.GlobalScope get structure.data - State(config.get, Nil, Set.empty, None, Nil, State.newHistory, AttributeMap.empty, State.Continue) + State(config.get, Nil, Set.empty, None, Nil, State.newHistory, AttributeMap.empty, StandardMain.initialGlobalLogging, State.Continue) } } diff --git a/main/command/BasicCommandStrings.scala b/main/command/BasicCommandStrings.scala new file mode 100644 index 000000000..ba1c32010 --- /dev/null +++ b/main/command/BasicCommandStrings.scala @@ -0,0 +1,126 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt + +import complete.HistoryCommands +import scala.annotation.tailrec + +import java.io.File +import Path._ + +object BasicCommandStrings +{ + val HelpCommand = "help" + val Exit = "exit" + val Quit = "quit" + + /** The command name to terminate the program.*/ + val TerminateAction: String = Exit + + 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. +Otherwise, this prints a help summary.""" + + def historyHelp = Help.briefDetail(HistoryCommands.descriptions) + + def exitBrief = "Terminates the build." + + def ReadCommand = "<" + def ReadFiles = " file1 file2 ..." + def ReadBrief = (ReadCommand + " *", "Reads command lines from the provided files.") + def ReadDetailed = +ReadCommand + ReadFiles + """ + + Reads the lines from the given files and inserts them as commands. + All empty lines and lines that start with '#' are ignored. + If a file does not exist or is not readable, this command fails. + + All the lines from all the files are read before any of the commands + are executed. Thus, if any file is not readable, none of commands + from any of the files (even the existing ones) will be run. + + You probably need to escape this command if entering it at your shell.""" + + def ApplyCommand = "apply" + def ApplyBrief = (ApplyCommand + " *", ApplyDetailed) + def ApplyDetailed = "Transforms the current State by calling .apply(currentState) for each listed module name." + + def RebootCommand = "reboot" + def RebootSummary = RebootCommand + " [full]" + def RebootBrief = (RebootSummary, "Reboots sbt and then executes the remaining commands.") + def RebootDetailed = +RebootSummary + """ + + This command is equivalent to exiting sbt, restarting, and running the + remaining commands with the exception that the JVM is not shut down. + + If 'full' is specified, the boot directory (`~/.sbt/boot` by default) + is deleted before restarting. This forces an update of sbt and Scala + and is useful when working with development versions of sbt or Scala.""" + + def Multi = ";" + def MultiBrief = (Multi + " (" + Multi + " )*", "Runs the provided semicolon-separated commands.") + def MultiDetailed = +Multi + " command1 " + Multi + """ command2 ... + + Runs the specified commands.""" + + def AppendCommand = "append" + def AppendLastBrief = (AppendCommand + " ", AppendLastDetailed) + def AppendLastDetailed = "Appends 'command' to list of commands to run." + + 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, the corresponding `value` is run. + If any argument is provided to `name`, it is appended as argument to `value`. + +""" + +AliasCommand + """ name= + + Removes the alias for `name`.""" + + def Shell = "shell" + def ShellBrief = ShellDetailed + def ShellDetailed = "Provides an interactive prompt from which commands can be run." + + def ClearOnFailure = "--" + 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 resets when it runs once, so it must be added + again if desired.""" + + def IfLast = "iflast" + def IfLastBrief = (IfLast + " ", IfLastCommon) + def IfLastCommon = "If there are no more commands after this one, 'command' is run." + def IfLastDetailed = +IfLast + """ command + + """ + IfLastCommon + + val ContinuousExecutePrefix = "~" + def continuousBriefHelp = (ContinuousExecutePrefix + " ", "Executes the specified command whenever source files change.") +} diff --git a/main/command/BasicCommands.scala b/main/command/BasicCommands.scala new file mode 100644 index 000000000..5d0c29e42 --- /dev/null +++ b/main/command/BasicCommands.scala @@ -0,0 +1,223 @@ +package sbt + + import complete.{DefaultParsers, HistoryCommands, Parser} + import DefaultParsers._ + import Types.{const,idFun} + import Function.tupled + import Command.applyEffect + import State.FailureWall + import HistoryCommands.{Start => HistoryPrefix} + import BasicCommandStrings._ + import CommandUtil._ + import BasicKeys._ + + import java.io.File + +object BasicCommands +{ + lazy val allBasicCommands = Seq(nop, ignore, help, multi, ifLast, append, setOnFailure, clearOnFailure, reboot, call, exit, continuous, history, shell, read, alias) + + def nop = Command.custom(s => success(() => s)) + def ignore = Command.command(FailureWall)(idFun) + + def help = Command.make(HelpCommand, helpBrief, helpDetailed)(helpParser) + + def helpParser(s: State) = + { + val h = (Help.empty /: s.definedCommands)(_ ++ _.help(s)) + val helpCommands = h.detail.keySet + val args = (token(Space) ~> token( NotSpace examples helpCommands )).* + applyEffect(args)(runHelp(s, h)) + } + + def runHelp(s: State, h: Help)(args: Seq[String]): State = + { + val message = + if(args.isEmpty) + aligned(" ", " ", h.brief).mkString("\n", "\n", "\n") + else + detail(args, h.detail) mkString("\n", "\n\n", "\n") + System.out.println(message) + s + } + def detail(selected: Seq[String], detailMap: Map[String, String]): Seq[String] = + selected.distinct flatMap { detailMap get _ } + + def multiParser(s: State): Parser[Seq[String]] = + { + val nonSemi = token(charClass(_ != ';').+, hide= const(true)) + ( token(';' ~> OptSpace) flatMap { _ => matched((s.combinedParser&nonSemi) | nonSemi) <~ token(OptSpace) } map (_.trim) ).+ + } + + def multiApplied(s: State) = + Command.applyEffect( multiParser(s) )( _ ::: s ) + + def multi = Command.custom(multiApplied, Help(Multi, MultiBrief, MultiDetailed) ) + + lazy val otherCommandParser = (s: State) => token(OptSpace ~> combinedLax(s, any.+) ) + def combinedLax(s: State, any: Parser[_]): Parser[String] = + matched(s.combinedParser | token(any, hide= const(true))) + + def ifLast = Command(IfLast, IfLastBrief, IfLastDetailed)(otherCommandParser) { (s, arg) => + if(s.remainingCommands.isEmpty) arg :: s else s + } + def append = Command(AppendCommand, AppendLastBrief, AppendLastDetailed)(otherCommandParser) { (s, arg) => + s.copy(remainingCommands = s.remainingCommands :+ arg) + } + + def setOnFailure = Command(OnFailure, OnFailureBrief, OnFailureDetailed)(otherCommandParser) { (s, arg) => + s.copy(onFailure = Some(arg)) + } + def clearOnFailure = Command.command(ClearOnFailure)(s => s.copy(onFailure = None)) + + def reboot = Command(RebootCommand, RebootBrief, RebootDetailed)(rebootParser) { (s, full) => + s.reboot(full) + } + def rebootParser(s: State) = token(Space ~> "full" ^^^ true) ?? false + + def call = Command(ApplyCommand, ApplyBrief, ApplyDetailed)(_ => spaceDelimited("")) { (state,args) => + val loader = getClass.getClassLoader + val loaded = args.map(arg => ModuleUtilities.getObject(arg, loader)) + (state /: loaded) { case (s, obj: (State => State)) => obj(s) } + } + + def exit = Command.command(TerminateAction, exitBrief, exitBrief ) ( _ exit true ) + + + def continuous = + Command(ContinuousExecutePrefix, Help(continuousBriefHelp) )(otherCommandParser) { (s, arg) => + withAttribute(s, Watched.Configuration, "Continuous execution not configured.") { w => + val repeat = ContinuousExecutePrefix + (if(arg.startsWith(" ")) arg else " " + arg) + Watched.executeContinuously(w, s, arg, repeat) + } + } + + def history = Command.custom(historyParser, BasicCommandStrings.historyHelp) + def historyParser(s: State): Parser[() => State] = + Command.applyEffect(HistoryCommands.actionParser) { histFun => + val logError = (msg: String) => s.log.error(msg) + val hp = s get historyPath getOrElse None + val lines = hp.toList.flatMap( p => IO.readLines(p) ).toIndexedSeq + histFun( complete.History(lines, hp, logError) ) match + { + case Some(commands) => + commands foreach println //printing is more appropriate than logging + (commands ::: s).continue + case None => s.fail + } + } + + def shell = Command.command(Shell, ShellBrief, ShellDetailed) { s => + val history = (s get historyPath) getOrElse Some(new File(s.baseDir, ".history")) + val prompt = (s get shellPrompt) match { case Some(pf) => pf(s); case None => "> " } + val reader = new FullReader(history, s.combinedParser) + val line = reader.readLine(prompt) + line match { + case Some(line) => + val newState = s.copy(onFailure = Some(Shell), remainingCommands = line +: Shell +: s.remainingCommands) + if(line.trim.isEmpty) newState else newState.clearGlobalLog + case None => s + } + } + + def read = Command.make(ReadCommand, ReadBrief, ReadDetailed)(s => applyEffect(readParser(s))(doRead(s)) ) + def readParser(s: State) = + { + val files = (token(Space) ~> fileParser(s.baseDir)).+ + val portAndSuccess = token(OptSpace) ~> Port + portAndSuccess || files + } + def doRead(s: State)(arg: Either[Int, Seq[File]]): State = + arg match + { + case Left(portAndSuccess) => + val port = math.abs(portAndSuccess) + val previousSuccess = portAndSuccess >= 0 + readMessage(port, previousSuccess) match + { + case Some(message) => (message :: (ReadCommand + " " + port) :: s).copy(onFailure = Some(ReadCommand + " " + (-port))) + case None => + System.err.println("Connection closed.") + s.fail + } + case Right(from) => + 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 { + s.log.error("Command file(s) not readable: \n\t" + notFound.mkString("\n\t")) + s + } + } + private def readMessage(port: Int, previousSuccess: Boolean): Option[String] = + { + // split into two connections because this first connection ends the previous communication + xsbt.IPC.client(port) { _.send(previousSuccess.toString) } + // and this second connection starts the next communication + xsbt.IPC.client(port) { ipc => + val message = ipc.receive + if(message eq null) None else Some(message) + } + } + + + def alias = Command.make(AliasCommand, AliasBrief, AliasDetailed) { s => + val name = token(OpOrID.examples( aliasNames(s) : _*) ) + val assign = token(OptSpace ~ '=' ~ OptSpace) + val sfree = removeAliases(s) + val to = matched(sfree.combinedParser, partial = true) | any.+.string + val base = (OptSpace ~> (name ~ (assign ~> to.?).?).?) + applyEffect(base)(t => runAlias(s, t) ) + } + + def runAlias(s: State, args: Option[(String, Option[Option[String]])]): State = + args match + { + case None => printAliases(s); s + case Some(x ~ None) if !x.isEmpty => printAlias(s, x.trim); s + case Some(name ~ Some(None)) => removeAlias(s, name.trim) + case Some(name ~ Some(Some(value))) => addAlias(s, name.trim, value.trim) + } + def addAlias(s: State, name: String, value: String): State = + if(Command validID name) { + val removed = removeAlias(s, name) + if(value.isEmpty) removed else removed.copy(definedCommands = newAlias(name, value) +: removed.definedCommands) + } else { + System.err.println("Invalid alias name '" + name + "'.") + s.fail + } + + def removeAliases(s: State): State = removeTagged(s, CommandAliasKey) + def removeAlias(s: State, name: String): State = s.copy(definedCommands = s.definedCommands.filter(c => !isAliasNamed(name, c)) ) + + def removeTagged(s: State, tag: AttributeKey[_]): State = s.copy(definedCommands = removeTagged(s.definedCommands, tag)) + def removeTagged(as: Seq[Command], tag: AttributeKey[_]): Seq[Command] = as.filter(c => ! (c.tags contains tag)) + + def isAliasNamed(name: String, c: Command): Boolean = isNamed(name, getAlias(c)) + def isNamed(name: String, alias: Option[(String,String)]): Boolean = alias match { case None => false; case Some((n,_)) => name == n } + + def getAlias(c: Command): Option[(String,String)] = c.tags get CommandAliasKey + def printAlias(s: State, name: String): Unit = printAliases(aliases(s,(n,v) => n == name) ) + def printAliases(s: State): Unit = printAliases(allAliases(s)) + def printAliases(as: Seq[(String,String)]): Unit = + for( (name,value) <- as) + println("\t" + name + " = " + value) + + def aliasNames(s: State): Seq[String] = allAliases(s).map(_._1) + def allAliases(s: State): Seq[(String,String)] = aliases(s, (n,v) => true) + def aliases(s: State, pred: (String,String) => Boolean): Seq[(String,String)] = + s.definedCommands.flatMap(c => getAlias(c).filter(tupled(pred))) + + def newAlias(name: String, value: String): Command = + Command.make(name, (name, "'" + value + "'"), "Alias of '" + value + "'")(aliasBody(name, value)).tag(CommandAliasKey, (name, value)) + def aliasBody(name: String, value: String)(state: State): Parser[() => State] = + OptSpace ~> Parser(Command.combine(removeAlias(state,name).definedCommands)(state))(value) + + def delegateToAlias(name: String, orElse: Parser[() => State])(state: State): Parser[() => State] = + aliases(state, (nme,_) => nme == name).headOption match { + case None => orElse + case Some((n,v)) => aliasBody(n,v)(state) + } + + val CommandAliasKey = AttributeKey[(String,String)]("is-command-alias", "Internal: marker for Commands created as aliases for another command.") +} \ No newline at end of file diff --git a/main/command/BasicKeys.scala b/main/command/BasicKeys.scala new file mode 100644 index 000000000..b7f9fa104 --- /dev/null +++ b/main/command/BasicKeys.scala @@ -0,0 +1,10 @@ +package sbt + + import java.io.File + +object BasicKeys +{ + val historyPath = AttributeKey[Option[File]]("history", "The location where command line history is persisted.") + val shellPrompt = AttributeKey[State => String]("shell-prompt", "The function that constructs the command prompt from the current build state.") + val watch = AttributeKey[Watched]("watch", "Continuous execution configuration.") +} diff --git a/main/Command.scala b/main/command/Command.scala similarity index 100% rename from main/Command.scala rename to main/command/Command.scala diff --git a/main/command/CommandUtil.scala b/main/command/CommandUtil.scala new file mode 100644 index 000000000..b600a5948 --- /dev/null +++ b/main/command/CommandUtil.scala @@ -0,0 +1,32 @@ +package sbt + + import java.io.File + +object CommandUtil +{ + 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("#") + + private def canRead = (_: File).canRead + def notReadable(files: Seq[File]): Seq[File] = files filterNot canRead + def readable(files: Seq[File]): Seq[File] = files filter canRead + + // slightly better fallback in case of older launcher + def bootDirectory(state: State): File = + try { state.configuration.provider.scalaProvider.launcher.bootDirectory } + catch { case e: NoSuchMethodError => new File(".").getAbsoluteFile } + + def aligned(pre: String, sep: String, in: Seq[(String, String)]): Seq[String] = + { + val width = in.map(_._1.length).max + in.map { case (a, b) => (" " + fill(a, width) + sep + b) } + } + def fill(s: String, size: Int) = s + " " * math.max(size - s.length, 0) + + def withAttribute[T](s: State, key: AttributeKey[T], ifMissing: String)(f: T => State): State = + (s get key) match { + case None => s.log.error(ifMissing); s.fail + case Some(nav) => f(nav) + } +} \ No newline at end of file diff --git a/main/MainControl.scala b/main/command/MainControl.scala similarity index 100% rename from main/MainControl.scala rename to main/command/MainControl.scala diff --git a/main/command/MainLoop.scala b/main/command/MainLoop.scala new file mode 100644 index 000000000..1e1048598 --- /dev/null +++ b/main/command/MainLoop.scala @@ -0,0 +1,100 @@ +/* sbt -- Simple Build Tool + * Copyright 2008, 2009, 2010, 2011 Mark Harrah + */ +package sbt + + import scala.annotation.tailrec + import java.io.{File, PrintWriter} + import java.lang.reflect.InvocationTargetException + +object MainLoop +{ + /** Entry point to run the remaining commands in State with managed global logging.*/ + def runLogged(state: State): xsbti.MainResult = + runLoggedLoop(state, state.globalLogging.backing) + + /** Run loop that evaluates remaining commands and manages changes to global logging configuration.*/ + @tailrec def runLoggedLoop(state: State, logBacking: GlobalLogBacking): xsbti.MainResult = + runAndClearLast(state, logBacking) match { + case ret: Return => // delete current and last log files when exiting normally + logBacking.file.delete() + deleteLastLog(logBacking) + ret.result + case clear: ClearGlobalLog => // delete previous log file, move current to previous, and start writing to a new file + deleteLastLog(logBacking) + runLoggedLoop(clear.state, logBacking.shiftNew()) + case keep: KeepGlobalLog => // make previous log file the current log file + logBacking.file.delete + runLoggedLoop(keep.state, logBacking.unshift) + } + + /** Runs the next sequence of commands, cleaning up global logging after any exceptions. */ + def runAndClearLast(state: State, logBacking: GlobalLogBacking): RunNext = + try + runWithNewLog(state, logBacking) + catch { + case e: xsbti.FullReload => + deleteLastLog(logBacking) + throw e // pass along a reboot request + case e => + System.err.println("sbt appears to be exiting abnormally.\n The log file for this session is at " + logBacking.file) + deleteLastLog(logBacking) + throw e + } + + /** Deletes the previous global log file. */ + def deleteLastLog(logBacking: GlobalLogBacking): Unit = + logBacking.last.foreach(_.delete()) + + /** Runs the next sequence of commands with global logging in place. */ + def runWithNewLog(state: State, logBacking: GlobalLogBacking): RunNext = + Using.fileWriter(append = true)(logBacking.file) { writer => + val out = new java.io.PrintWriter(writer) + val loggedState = state.copy(globalLogging = logBacking.newLogger(out, logBacking)) + try run(loggedState) finally out.close() + } + sealed trait RunNext + final class ClearGlobalLog(val state: State) extends RunNext + final class KeepGlobalLog(val state: State) extends RunNext + final class Return(val result: xsbti.MainResult) extends RunNext + + /** Runs the next sequence of commands that doesn't require global logging changes.*/ + @tailrec def run(state: State): RunNext = + state.next match + { + case State.Continue => run(next(state)) + case State.ClearGlobalLog => new ClearGlobalLog(state.continue) + case State.KeepLastLog => new KeepGlobalLog(state.continue) + case ret: State.Return => new Return(ret.result) + } + + def next(state: State): State = + ErrorHandling.wideConvert { state.process(Command.process) } match + { + case Right(s) => s + case Left(t: xsbti.FullReload) => throw t + case Left(t) => handleException(t, state) + } + + def handleException(e: Throwable, s: State): State = + handleException(e, s, s.log) + def handleException(e: Throwable, s: State, log: Logger): State = + { + e match + { + case _: AlreadyHandledException | _: UnprintableException => () + case ite: InvocationTargetException => + val cause = ite.getCause + if(cause == null || cause == ite) logFullException(ite, log) else handleException(cause, s, log) + case _: MessageOnlyException => log.error(e.toString) + case _ => logFullException(e, log) + } + s.fail + } + def logFullException(e: Throwable, log: Logger) + { + log.trace(e) + log.error(ErrorHandling reducedToString e) + log.error("Use 'last' for the full log.") + } +} diff --git a/main/State.scala b/main/command/State.scala similarity index 99% rename from main/State.scala rename to main/command/State.scala index 6d81f07ac..10ef7ad50 100644 --- a/main/State.scala +++ b/main/command/State.scala @@ -5,7 +5,6 @@ package sbt import java.io.File import java.util.concurrent.Callable - import CommandSupport.{FailureWall, logger} /** Data structure representing all command execution information. @@ -27,6 +26,7 @@ final case class State( remainingCommands: Seq[String], history: State.History, attributes: AttributeMap, + globalLogging: GlobalLogging, next: State.Next ) extends Identity { lazy val combinedParser = Command.combine(definedCommands)(this) @@ -112,6 +112,8 @@ trait StateOps { object State { + final val FailureWall = "---" + /** Represents the next action for the command processor.*/ sealed trait Next /** Indicates that the command processor should process the next command.*/ @@ -175,7 +177,7 @@ object State def update[T](key: AttributeKey[T])(f: Option[T] => T): State = put(key, f(get(key))) def has(key: AttributeKey[_]) = s.attributes contains key def remove(key: AttributeKey[_]) = s.copy(attributes = s.attributes remove key) - def log = CommandSupport.logger(s) + def log = s.globalLogging.full def fail = { val remaining = s.remainingCommands.dropWhile(_ != FailureWall) diff --git a/main/Watched.scala b/main/command/Watched.scala similarity index 96% rename from main/Watched.scala rename to main/command/Watched.scala index 539dc10c0..dc3d90c50 100644 --- a/main/Watched.scala +++ b/main/command/Watched.scala @@ -3,7 +3,8 @@ */ package sbt - import CommandSupport.{ClearOnFailure,FailureWall} + import BasicCommandStrings.ClearOnFailure + import State.FailureWall import annotation.tailrec import java.io.File import Types.const @@ -63,7 +64,7 @@ object Watched catch { case e: Exception => val log = s.log log.error("Error occurred obtaining files to watch. Terminating continuous execution...") - BuiltinCommands.handleException(e, s, log) + MainLoop.handleException(e, s, log) (false, watchState, s.fail) } diff --git a/project/Sbt.scala b/project/Sbt.scala index fef29065a..aba301339 100644 --- a/project/Sbt.scala +++ b/project/Sbt.scala @@ -111,8 +111,11 @@ object Sbt extends Build classfileSub, classpathSub, compileIncrementalSub, compilePersistSub, compilerSub, completeSub, apiSub, interfaceSub, ioSub, ivySub, logSub, processSub, runSub, stdTaskSub, taskSub, trackingSub, testingSub) + lazy val commandSub = testedBaseProject(commandPath, "Command") dependsOn(interfaceSub, ioSub, launchInterfaceSub, logSub, completeSub, classpathSub) + // The main integration project for sbt. It brings all of the subsystems together, configures them, and provides for overriding conventions. - lazy val mainSub = testedBaseProject(mainPath, "Main") dependsOn(actionsSub, interfaceSub, ioSub, ivySub, launchInterfaceSub, logSub, processSub, runSub) + lazy val mainSub = testedBaseProject(mainPath, "Main") dependsOn(actionsSub, interfaceSub, ioSub, ivySub, launchInterfaceSub, logSub, processSub, runSub, commandSub) + // Strictly for bringing implicits and aliases from subsystems into the top-level sbt namespace through a single package object // technically, we need a dependency on all of mainSub's dependencies, but we don't do that since this is strictly an integration project // with the sole purpose of providing certain identifiers without qualification (with a package object) @@ -126,6 +129,7 @@ object Sbt extends Build def utilPath = file("util") def compilePath = file("compile") def mainPath = file("main") + def commandPath = mainPath / "command" def scriptedPath = file("scripted") def sbtSettings = Seq( diff --git a/sbt/package.scala b/sbt/package.scala index e36dfafef..d53be82a5 100644 --- a/sbt/package.scala +++ b/sbt/package.scala @@ -4,6 +4,9 @@ package object sbt extends sbt.std.TaskExtra with sbt.Types with sbt.ProcessExtra with sbt.impl.DependencyBuilders with sbt.PathExtra with sbt.ProjectExtra with sbt.DependencyFilterExtra with sbt.BuildExtra { + @deprecated("Renamed to CommandStrings.", "0.12.0") + val CommandSupport = CommandStrings + @deprecated("Use SettingKey, which is a drop-in replacement.", "0.11.1") type ScopedSetting[T] = SettingKey[T] @deprecated("Use TaskKey, which is a drop-in replacement.", "0.11.1") diff --git a/tasks/Incomplete.scala b/tasks/Incomplete.scala index cb9e9ca08..b010b8ade 100644 --- a/tasks/Incomplete.scala +++ b/tasks/Incomplete.scala @@ -13,7 +13,7 @@ import Incomplete.{Error, Value => IValue} * @param causes a list of incompletions that prevented `node` from completing * @param directCause the exception that caused `node` to not complete */ final case class Incomplete(node: Option[AnyRef], tpe: IValue = Error, message: Option[String] = None, causes: Seq[Incomplete] = Nil, directCause: Option[Throwable] = None) - extends Exception(message.orNull, directCause.orNull) { + extends Exception(message.orNull, directCause.orNull) with UnprintableException { override def toString = "Incomplete(node=" + node + ", tpe=" + tpe + ", msg=" + message + ", causes=" + causes + ", directCause=" + directCause +")" } diff --git a/util/collection/Settings.scala b/util/collection/Settings.scala index 4111c9082..d8945a787 100644 --- a/util/collection/Settings.scala +++ b/util/collection/Settings.scala @@ -177,7 +177,7 @@ trait Init[Scope] if(dist < 0) None else Some(dist) } - final class Uninitialized(val undefined: Seq[Undefined], msg: String) extends Exception(msg) + final class Uninitialized(val undefined: Seq[Undefined], override val toString: String) extends Exception(toString) final class Undefined(val definingKey: ScopedKey[_], val referencedKey: ScopedKey[_]) final class RuntimeUndefined(val undefined: Seq[Undefined]) extends RuntimeException("References to undefined settings at runtime.") def Undefined(definingKey: ScopedKey[_], referencedKey: ScopedKey[_]): Undefined = new Undefined(definingKey, referencedKey) diff --git a/util/control/MessageOnlyException.scala b/util/control/MessageOnlyException.scala index 6791a9339..7fa43746d 100644 --- a/util/control/MessageOnlyException.scala +++ b/util/control/MessageOnlyException.scala @@ -4,4 +4,11 @@ package sbt final class MessageOnlyException(override val toString: String) extends RuntimeException(toString) -final class NoMessageException extends RuntimeException \ No newline at end of file + +/** A dummy exception for the top-level exception handler to know that an exception +* has been handled, but is being passed further up to indicate general failure. */ +final class AlreadyHandledException extends RuntimeException + +/** A marker trait for a top-level exception handler to know that this exception +* doesn't make sense to display. */ +trait UnprintableException extends Throwable \ No newline at end of file diff --git a/util/log/GlobalLogging.scala b/util/log/GlobalLogging.scala new file mode 100644 index 000000000..e54b00a00 --- /dev/null +++ b/util/log/GlobalLogging.scala @@ -0,0 +1,27 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt + + import java.io.{File, PrintWriter} + +final case class GlobalLogging(full: Logger, backed: ConsoleLogger, backing: GlobalLogBacking) +final case class GlobalLogBacking(file: File, last: Option[File], newLogger: (PrintWriter, GlobalLogBacking) => GlobalLogging, newBackingFile: () => File) +{ + def shift(newFile: File) = GlobalLogBacking(newFile, Some(file), newLogger, newBackingFile) + def shiftNew() = shift(newBackingFile()) + def unshift = GlobalLogBacking(last getOrElse file, None, newLogger, newBackingFile) +} +object GlobalLogBacking +{ + def apply(newLogger: (PrintWriter, GlobalLogBacking) => GlobalLogging, newBackingFile: => File): GlobalLogBacking = + GlobalLogBacking(newBackingFile, None, newLogger, newBackingFile _) +} +object GlobalLogging +{ + def initial(newLogger: (PrintWriter, GlobalLogBacking) => GlobalLogging, newBackingFile: => File): GlobalLogging = + { + val log = ConsoleLogger() + GlobalLogging(log, log, GlobalLogBacking(newLogger, newBackingFile)) + } +} \ No newline at end of file diff --git a/util/log/MainLogging.scala b/util/log/MainLogging.scala new file mode 100644 index 000000000..b07abf4e3 --- /dev/null +++ b/util/log/MainLogging.scala @@ -0,0 +1,36 @@ +package sbt + + import java.io.PrintWriter + +object MainLogging +{ + def multiLogger(config: MultiLoggerConfig): Logger = + { + import config._ + val multi = new MultiLogger(console :: backed :: extra) + // sets multi to the most verbose for clients that inspect the current level + multi setLevel Level.unionAll(backingLevel :: screenLevel :: extra.map(_.getLevel)) + // set the specific levels + console setLevel screenLevel + backed setLevel backingLevel + console setTrace screenTrace + backed setTrace backingTrace + multi: Logger + } + def globalDefault(writer: PrintWriter, backing: GlobalLogBacking): GlobalLogging = + { + val backed = defaultBacked()(writer) + val full = multiLogger(defaultMultiConfig( backed ) ) + GlobalLogging(full, backed, backing) + } + + def defaultMultiConfig(backing: AbstractLogger): MultiLoggerConfig = + new MultiLoggerConfig(defaultScreen, backing, Nil, Level.Info, Level.Debug, -1, Int.MaxValue) + + def defaultScreen: AbstractLogger = ConsoleLogger() + + def defaultBacked(useColor: Boolean = ConsoleLogger.formatEnabled): PrintWriter => ConsoleLogger = + to => ConsoleLogger(ConsoleLogger.printWriterOut(to), useColor = useColor) // TODO: should probably filter ANSI codes when useColor=false +} + +final case class MultiLoggerConfig(console: AbstractLogger, backed: AbstractLogger, extra: List[AbstractLogger], screenLevel: Level.Value, backingLevel: Level.Value, screenTrace: Int, backingTrace: Int) \ No newline at end of file