split command core to main/command/

This commit is contained in:
Mark Harrah 2012-01-29 14:36:27 -05:00
parent bf472b8ff4
commit 98c98f9c26
26 changed files with 662 additions and 556 deletions

View File

@ -11,7 +11,7 @@ package sbt
import DefaultParsers._ import DefaultParsers._
import Types.idFun import Types.idFun
import java.net.URI import java.net.URI
import CommandSupport.ShowCommand import CommandStrings.ShowCommand
final class ParsedKey(val key: ScopedKey[_], val mask: ScopeMask) final class ParsedKey(val key: ScopedKey[_], val mask: ScopeMask)
object Act object Act

View File

@ -3,37 +3,15 @@
*/ */
package sbt package sbt
import complete.HistoryCommands object CommandStrings
import scala.annotation.tailrec
import java.io.File
import Path._
object CommandSupport
{ {
def logger(s: State) = globalLogging(s).full @deprecated("Use the `log` member of a State instance directly.", "0.12.0")
def globalLogging(s: State) = s get Keys.globalLogging getOrElse error("Global logging misconfigured") def logger(s: State) = s.log
// slightly better fallback in case of older launcher @deprecated("Use the `globalLogging` member of a State instance directly.", "0.12.0")
def bootDirectory(state: State): File = def globalLogging(s: State) = s.globalLogging
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("#")
/** The prefix used to identify a request to execute the remaining input on source changes.*/ /** The prefix used to identify a request to execute the remaining input on source changes.*/
val ContinuousExecutePrefix = "~"
val HelpCommand = "help"
val AboutCommand = "about" val AboutCommand = "about"
val TasksCommand = "tasks" val TasksCommand = "tasks"
val ProjectCommand = "project" val ProjectCommand = "project"
@ -41,8 +19,14 @@ object CommandSupport
val ShowCommand = "show" val ShowCommand = "show"
val BootCommand = "boot" val BootCommand = "boot"
val Exit = "exit" @deprecated("Moved to BasicCommandStrings", "0.12.0")
val Quit = "quit" 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 EvalCommand = "eval"
val evalBrief = (EvalCommand + " <expression>", "Evaluates the given Scala expression and prints the result and type.") val evalBrief = (EvalCommand + " <expression>", "Evaluates the given Scala expression and prints the result and type.")
@ -128,9 +112,8 @@ SetCommand + """ <setting-expression>
def sessionBrief = (SessionCommand + " <session-command>", "Manipulates session settings. For details, run 'help " + SessionCommand + "'.") def sessionBrief = (SessionCommand + " <session-command>", "Manipulates session settings. For details, run 'help " + SessionCommand + "'.")
/** The command name to terminate the program.*/ /** The command name to terminate the program.*/
val TerminateAction: String = Exit @deprecated("Moved to BasicCommandStrings", "0.12.0")
val TerminateAction: String = BasicCommandStrings.TerminateAction
def continuousBriefHelp = (ContinuousExecutePrefix + " <command>", "Executes the specified command whenever source files change.")
def tasksPreamble = """ def tasksPreamble = """
This is a list of tasks defined for the current project. 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 tasksBrief = "Displays the tasks defined for the current project."
def tasksDetailed = "Displays the tasks defined directly or indirectly 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 aboutBrief = "Displays basic information about sbt and the build."
def aboutDetailed = aboutBrief def aboutDetailed = aboutBrief
@ -175,126 +153,23 @@ ProjectCommand +
def projectsBrief = projectsDetailed def projectsBrief = projectsDetailed
def projectsDetailed = "Displays the names of available projects." def projectsDetailed = "Displays the names of available projects."
def historyHelp = Help.briefDetail(HistoryCommands.descriptions)
def exitBrief = "Terminates the build."
def sbtrc = ".sbtrc" def sbtrc = ".sbtrc"
def ReadCommand = "<" @deprecated("Moved to BasicCommandStrings", "0.12.0")
def ReadFiles = " file1 file2 ..." def ReadCommand = BasicCommandStrings.ReadCommand
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.
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 + " <module-name>*", ApplyDetailed)
def ApplyDetailed = "Transforms the current State by calling <module-name>.apply(currentState) for each listed."
def DefaultsCommand = "add-default-commands" def DefaultsCommand = "add-default-commands"
def DefaultsBrief = (DefaultsCommand, DefaultsDetailed) def DefaultsBrief = (DefaultsCommand, DefaultsDetailed)
def DefaultsDetailed = "Registers default built-in commands" def DefaultsDetailed = "Registers default built-in commands"
@deprecated("Moved to BasicCommandStrings", "0.12.0")
def RebootCommand = "reboot" 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 Multi = ";"
def MultiBrief = (Multi + " <command> (" + Multi + " <command>)*", "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 AppendCommand = "append"
def AppendLastBrief = (AppendCommand + " <command>", 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 <names>] [-annot <names>]"
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 <paths> [-cp <paths>] [-d <path>]"
def CompileDetailed =
CompileSyntax + """
Incrementally compiles Scala and Java sources.
<paths> are explicit paths separated by the platform path separator.
The specified output path will contain the following directory structure:
scala_<version>/
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 Load = "load"
def LoadLabel = "a project" def LoadLabel = "a project"
@ -308,31 +183,20 @@ CompileSyntax + """
def LoadProjectBrief = (LoadProject, LoadProjectDetailed) def LoadProjectBrief = (LoadProject, LoadProjectDetailed)
def LoadProjectDetailed = "Loads the project in the current directory" def LoadProjectDetailed = "Loads the project in the current directory"
def Shell = "shell" @deprecated("Moved to State", "0.12.0")
def ShellBrief = ShellDetailed val FailureWall = State.FailureWall
def ShellDetailed = "Provides an interactive prompt from which commands can be run."
@deprecated("Moved to BasicCommandStrings", "0.12.0")
def Shell = BasicCommandStrings.Shell
@deprecated("Moved to BasicCommandStrings", "0.12.0")
def ClearOnFailure = "--" def ClearOnFailure = "--"
@deprecated("Moved to BasicCommandStrings", "0.12.0")
def OnFailure = "-" 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 IfLast = "iflast"
def IfLastBrief = (IfLast + " <command>", IfLastCommon)
def IfLastCommon = "If there are no more commands after this one, 'command' is run."
def IfLastDetailed =
IfLast + """ command
""" + IfLastCommon
def InitCommand = "initialize" def InitCommand = "initialize"
def InitBrief = (InitCommand, "Initializes command processing.") def InitBrief = (InitCommand, "Initializes command processing.")
@ -352,4 +216,12 @@ load-commands -base ~/.sbt/commands
< .sbtrc < .sbtrc
Runs commands from ~/.sbtrc and ./.sbtrc if they exist 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
} }

View File

@ -14,7 +14,7 @@ object IvyConsole
lazy val command = lazy val command =
Command.command(Name) { state => Command.command(Name) { state =>
val Dependencies(managed, repos, unmanaged) = parseDependencies(state.remainingCommands, state.log) 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) IO.createDirectory(base)
val (eval, structure) = Load.defaultLoad(state, base, state.log) val (eval, structure) = Load.defaultLoad(state, base, state.log)

View File

@ -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.") // 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 // Command keys
val globalLogging = AttributeKey[GlobalLogging]("global-logging", "Provides a global Logger, including command logging.") val historyPath = SettingKey(BasicKeys.historyPath)
val historyPath = SettingKey[Option[File]]("history", "The location where command line history is persisted.") val shellPrompt = SettingKey(BasicKeys.shellPrompt)
val shellPrompt = SettingKey[State => String]("shell-prompt", "The function that constructs the command prompt from the current build state.")
val analysis = AttributeKey[inc.Analysis]("analysis", "Analysis of compilation, including dependencies and generated outputs.") 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 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 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.") val watchTransitiveSources = TaskKey[Seq[File]]("watch-transitive-sources", "Defines the sources in all projects for continuous execution to watch.")

View File

@ -14,7 +14,7 @@ package sbt
import inc.{FileValueCache, Locate} import inc.{FileValueCache, Locate}
import Project.{inScope, ScopedKey, ScopeLocal, Setting} import Project.{inScope, ScopedKey, ScopeLocal, Setting}
import Keys.{appConfiguration, baseDirectory, configuration, streams, Streams, thisProject, thisProjectRef} 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 tools.nsc.reporters.ConsoleReporter
import Build.{analyzed, data} import Build.{analyzed, data}
import Scope.{GlobalScope, ThisScope} 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 inputs = Compiler.inputs(data(classpath), sources, target, Nil, Nil, definesClass, Compiler.DefaultMaxErrors, CompileOrder.Mixed)(compilers, log)
val analysis = val analysis =
try { Compiler(inputs, log) } 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) (inputs, analysis)
} }

View File

@ -9,6 +9,7 @@ package sbt
import std.Transform import std.Transform
import Project.ScopedKey import Project.ScopedKey
import Scope.GlobalScope import Scope.GlobalScope
import MainLogging._
import Keys.{logLevel, logManager, persistLogLevel, persistTraceLevel, state, traceLevel} import Keys.{logLevel, logManager, persistLogLevel, persistTraceLevel, state, traceLevel}
object LogManager object LogManager
@ -21,11 +22,6 @@ object LogManager
lazy val default: LogManager = withLoggers() lazy val default: LogManager = withLoggers()
def defaults(extra: ScopedKey[_] => Seq[AbstractLogger]): LogManager = withLoggers(extra = extra) 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 withScreenLogger(mk: => AbstractLogger): LogManager = withLoggers(mk)
def withLoggers(screen: => AbstractLogger = defaultScreen, backed: PrintWriter => AbstractLogger = defaultBacked(), extra: ScopedKey[_] => Seq[AbstractLogger] = _ => Nil): LogManager = 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 backingLevel = getOr(persistLogLevel.key, Level.Debug)
val screenTrace = getOr(traceLevel.key, -1) val screenTrace = getOr(traceLevel.key, -1)
val backingTrace = getOr(persistTraceLevel.key, Int.MaxValue) 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) ) 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 trait LogManager
{ {
def apply(data: Settings[Scope], state: State, task: ScopedKey[_], writer: PrintWriter): Logger 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)

View File

@ -3,132 +3,64 @@
*/ */
package sbt package sbt
import Execute.NodeView import complete.{DefaultParsers, Parser}
import complete.{DefaultParsers, HistoryCommands, Parser}
import HistoryCommands.{Start => HistoryPrefix}
import compiler.EvalImports import compiler.EvalImports
import Types.{const,idFun} import Types.idFun
import Aggregation.AnyKeys import Aggregation.AnyKeys
import Command.applyEffect
import Keys.{analysis,historyPath,globalLogging,shellPrompt}
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.collection.JavaConversions._
import Function.tupled
import java.net.URI
import java.lang.reflect.InvocationTargetException
import Path._ import Path._
import StandardMain._
import java.io.File import java.io.File
import java.net.URI
/** This class is the entry point for sbt.*/ /** This class is the entry point for sbt.*/
final class xMain extends xsbti.AppMain final class xMain extends xsbti.AppMain
{ {
def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
{ {
import BuiltinCommands.{initialAttributes, initialize, defaults, DefaultBootCommands} import BuiltinCommands.{initialize, defaults}
import CommandSupport.{BootCommand, DefaultsCommand, InitCommand} import CommandStrings.{BootCommand, DefaultsCommand, InitCommand}
val initialCommandDefs = Seq(initialize, defaults) MainLoop.runLogged( initialState(configuration,
val commands = DefaultsCommand +: InitCommand +: BootCommand +: configuration.arguments.map(_.trim) Seq(initialize, defaults),
val state = State( configuration, initialCommandDefs, Set.empty, None, commands, State.newHistory, initialAttributes, State.Continue ) DefaultsCommand :: InitCommand :: BootCommand :: Nil)
MainLoop.runLogged(state) )
} }
} }
final class ScriptMain extends xsbti.AppMain final class ScriptMain extends xsbti.AppMain
{ {
def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
{ MainLoop.runLogged( initialState(configuration,
import BuiltinCommands.{initialAttributes, ScriptCommands} BuiltinCommands.ScriptCommands,
val commands = Script.Name +: configuration.arguments.map(_.trim) Script.Name :: Nil)
val state = State( configuration, ScriptCommands, Set.empty, None, commands, State.newHistory, initialAttributes, State.Continue ) )
MainLoop.runLogged(state)
}
} }
final class ConsoleMain extends xsbti.AppMain final class ConsoleMain extends xsbti.AppMain
{ {
def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
{ MainLoop.runLogged( initialState(configuration,
import BuiltinCommands.{initialAttributes, ConsoleCommands} BuiltinCommands.ConsoleCommands,
val commands = IvyConsole.Name +: configuration.arguments.map(_.trim) IvyConsole.Name :: Nil)
val state = State( configuration, ConsoleCommands, Set.empty, None, commands, State.newHistory, initialAttributes, State.Continue ) )
MainLoop.runLogged(state)
}
} }
object MainLoop
object StandardMain
{ {
/** Entry point to run the remaining commands in State with managed global logging.*/ def initialState(configuration: xsbti.AppConfiguration, initialDefinitions: Seq[Command], preCommands: Seq[String]): State =
def runLogged(state: State): xsbti.MainResult = {
runLoggedLoop(state, GlobalLogBacking(newBackingFile(), None)) val commands = preCommands ++ configuration.arguments.map(_.trim)
State( configuration, initialDefinitions, Set.empty, None, commands, State.newHistory, BuiltinCommands.initialAttributes, initialGlobalLogging, State.Continue )
/** Constructs a new, (weakly) unique, temporary file to use as the backing for global logging. */ }
def newBackingFile(): File = File.createTempFile("sbt",".log") def initialGlobalLogging: GlobalLogging =
GlobalLogging.initial(MainLogging.globalDefault _, 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)
}
} }
import DefaultParsers._ import DefaultParsers._
import CommandSupport._ import CommandStrings._
import BasicCommands._
import CommandUtil._
object BuiltinCommands object BuiltinCommands
{ {
def initialAttributes = AttributeMap.empty def initialAttributes = AttributeMap.empty
@ -140,22 +72,9 @@ object BuiltinCommands
def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil
def boot = Command.make(BootCommand)(bootParser) 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 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 // This parser schedules the default boot commands unless overridden by an alias
def bootParser(s: State) = def bootParser(s: State) =
{ {
@ -163,16 +82,6 @@ object BuiltinCommands
delegateToAlias(BootCommand, success(orElse) )(s) 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 sbtVersion(s: State): String = s.configuration.provider.id.version
def scalaVersion(s: State): String = s.configuration.provider.scalaProvider.version def scalaVersion(s: State): String = s.configuration.provider.scalaProvider.version
def aboutString(s: State): String = def aboutString(s: State): String =
@ -230,154 +139,15 @@ object BuiltinCommands
aligned(" ", " ", taskDetail(s)) mkString("\n", "\n", "") aligned(" ", " ", taskDetail(s)) mkString("\n", "\n", "")
def taskStrings(key: AttributeKey[_]): Option[(String, String)] = key.description map { d => (key.label, d) } 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 => def defaults = Command.command(DefaultsCommand) { s =>
s ++ DefaultCommands s ++ DefaultCommands
} }
def call = Command(ApplyCommand, ApplyBrief, ApplyDetailed)(_ => spaceDelimited("<class name>")) { (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 => def initialize = Command.command(InitCommand) { s =>
/*"load-commands -base ~/.sbt/commands" :: */readLines( readable( sbtRCs(s) ) ) ::: 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) => def eval = Command.single(EvalCommand, evalBrief, evalDetailed) { (s, arg) =>
val log = logger(s) val log = logger(s)
val extracted = Project extract 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. */ /** Determines the log file that last* commands should operate on. See also isLastOnly. */
def lastLogFile(s: State) = def lastLogFile(s: State) =
{ {
val backing = CommandSupport.globalLogging(s).backing val backing = s.globalLogging.backing
if(isLastOnly(s)) backing.last else Some(backing.file) if(isLastOnly(s)) backing.last else Some(backing.file)
} }
@ -514,7 +284,7 @@ object BuiltinCommands
} }
def act = Command.customHelp(Act.actParser, actHelp) 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 = def keysHelp(s: State): Help =
if(Project.isProjectLoaded(s)) if(Project.isProjectLoaded(s))
Help.detailOnly(taskDetail(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) for( (uri, build) <- structure.units if curi != uri) listBuild(uri, build, false, cid, log)
s 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 project = Command.make(ProjectCommand, projectBrief, projectDetailed)(ProjectNavigation.command)
def exit = Command.command(TerminateAction, exitBrief, exitBrief ) ( _ exit true )
def loadFailed = Command.command(LoadFailed)(handleLoadFailed) def loadFailed = Command.command(LoadFailed)(handleLoadFailed)
@tailrec def handleLoadFailed(s: State): State = @tailrec def handleLoadFailed(s: State): State =
{ {
@ -576,71 +339,4 @@ object BuiltinCommands
SessionSettings.checkSession(session, s) SessionSettings.checkSession(session, s)
Project.setProject(session, structure, 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.")
} }

View File

@ -203,7 +203,7 @@ object Project extends Init[Scope] with ProjectExtra
val prompt = get(shellPrompt) val prompt = get(shellPrompt)
val watched = get(watch) val watched = get(watch)
val commandDefs = allCommands.distinct.flatten[Command].map(_ tag (projectCommand, true)) 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) val newAttrs = setCond(Watched.Configuration, watched, s.attributes).put(historyPath.key, history)
s.copy(attributes = setCond(shellPrompt.key, prompt, newAttrs), definedCommands = newDefinedCommands) s.copy(attributes = setCond(shellPrompt.key, prompt, newAttrs), definedCommands = newDefinedCommands)
} }

View File

@ -16,7 +16,7 @@ object Script
val scriptArg = state.remainingCommands.headOption getOrElse error("No script file specified") val scriptArg = state.remainingCommands.headOption getOrElse error("No script file specified")
val script = new File(scriptArg).getAbsoluteFile val script = new File(scriptArg).getAbsoluteFile
val hash = Hash.halve(Hash.toHex(Hash(script.getAbsolutePath))) 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) IO.createDirectory(base)
val (eval, structure) = Load.defaultLoad(state, base, state.log) val (eval, structure) = Load.defaultLoad(state, base, state.log)

View File

@ -47,6 +47,6 @@ object TaskData
private[this] def fakeState(structure: BuildStructure): State = private[this] def fakeState(structure: BuildStructure): State =
{ {
val config = Keys.appConfiguration in Scope.GlobalScope get structure.data 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)
} }
} }

View File

@ -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 + " <file>*", "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 + " <module-name>*", ApplyDetailed)
def ApplyDetailed = "Transforms the current State by calling <module-name>.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 + " <command> (" + Multi + " <command>)*", "Runs the provided semicolon-separated commands.")
def MultiDetailed =
Multi + " command1 " + Multi + """ command2 ...
Runs the specified commands."""
def AppendCommand = "append"
def AppendLastBrief = (AppendCommand + " <command>", 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 + " <command>", 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 + " <command>", "Executes the specified command whenever source files change.")
}

View File

@ -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("<class name>")) { (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.")
}

View File

@ -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.")
}

View File

@ -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)
}
}

100
main/command/MainLoop.scala Normal file
View File

@ -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.")
}
}

View File

@ -5,7 +5,6 @@ package sbt
import java.io.File import java.io.File
import java.util.concurrent.Callable import java.util.concurrent.Callable
import CommandSupport.{FailureWall, logger}
/** /**
Data structure representing all command execution information. Data structure representing all command execution information.
@ -27,6 +26,7 @@ final case class State(
remainingCommands: Seq[String], remainingCommands: Seq[String],
history: State.History, history: State.History,
attributes: AttributeMap, attributes: AttributeMap,
globalLogging: GlobalLogging,
next: State.Next next: State.Next
) extends Identity { ) extends Identity {
lazy val combinedParser = Command.combine(definedCommands)(this) lazy val combinedParser = Command.combine(definedCommands)(this)
@ -112,6 +112,8 @@ trait StateOps {
object State object State
{ {
final val FailureWall = "---"
/** Represents the next action for the command processor.*/ /** Represents the next action for the command processor.*/
sealed trait Next sealed trait Next
/** Indicates that the command processor should process the next command.*/ /** 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 update[T](key: AttributeKey[T])(f: Option[T] => T): State = put(key, f(get(key)))
def has(key: AttributeKey[_]) = s.attributes contains key def has(key: AttributeKey[_]) = s.attributes contains key
def remove(key: AttributeKey[_]) = s.copy(attributes = s.attributes remove key) def remove(key: AttributeKey[_]) = s.copy(attributes = s.attributes remove key)
def log = CommandSupport.logger(s) def log = s.globalLogging.full
def fail = def fail =
{ {
val remaining = s.remainingCommands.dropWhile(_ != FailureWall) val remaining = s.remainingCommands.dropWhile(_ != FailureWall)

View File

@ -3,7 +3,8 @@
*/ */
package sbt package sbt
import CommandSupport.{ClearOnFailure,FailureWall} import BasicCommandStrings.ClearOnFailure
import State.FailureWall
import annotation.tailrec import annotation.tailrec
import java.io.File import java.io.File
import Types.const import Types.const
@ -63,7 +64,7 @@ object Watched
catch { case e: Exception => catch { case e: Exception =>
val log = s.log val log = s.log
log.error("Error occurred obtaining files to watch. Terminating continuous execution...") 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) (false, watchState, s.fail)
} }

View File

@ -111,8 +111,11 @@ object Sbt extends Build
classfileSub, classpathSub, compileIncrementalSub, compilePersistSub, compilerSub, completeSub, apiSub, classfileSub, classpathSub, compileIncrementalSub, compilePersistSub, compilerSub, completeSub, apiSub,
interfaceSub, ioSub, ivySub, logSub, processSub, runSub, stdTaskSub, taskSub, trackingSub, testingSub) 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. // 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 // 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 // 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) // 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 utilPath = file("util")
def compilePath = file("compile") def compilePath = file("compile")
def mainPath = file("main") def mainPath = file("main")
def commandPath = mainPath / "command"
def scriptedPath = file("scripted") def scriptedPath = file("scripted")
def sbtSettings = Seq( def sbtSettings = Seq(

View File

@ -4,6 +4,9 @@
package object sbt extends sbt.std.TaskExtra with sbt.Types with sbt.ProcessExtra with sbt.impl.DependencyBuilders 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 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") @deprecated("Use SettingKey, which is a drop-in replacement.", "0.11.1")
type ScopedSetting[T] = SettingKey[T] type ScopedSetting[T] = SettingKey[T]
@deprecated("Use TaskKey, which is a drop-in replacement.", "0.11.1") @deprecated("Use TaskKey, which is a drop-in replacement.", "0.11.1")

View File

@ -13,7 +13,7 @@ import Incomplete.{Error, Value => IValue}
* @param causes a list of incompletions that prevented `node` from completing * @param causes a list of incompletions that prevented `node` from completing
* @param directCause the exception that caused `node` to not complete */ * @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) 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 +")" override def toString = "Incomplete(node=" + node + ", tpe=" + tpe + ", msg=" + message + ", causes=" + causes + ", directCause=" + directCause +")"
} }

View File

@ -177,7 +177,7 @@ trait Init[Scope]
if(dist < 0) None else Some(dist) 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 Undefined(val definingKey: ScopedKey[_], val referencedKey: ScopedKey[_])
final class RuntimeUndefined(val undefined: Seq[Undefined]) extends RuntimeException("References to undefined settings at runtime.") 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) def Undefined(definingKey: ScopedKey[_], referencedKey: ScopedKey[_]): Undefined = new Undefined(definingKey, referencedKey)

View File

@ -4,4 +4,11 @@
package sbt package sbt
final class MessageOnlyException(override val toString: String) extends RuntimeException(toString) final class MessageOnlyException(override val toString: String) extends RuntimeException(toString)
final class NoMessageException extends RuntimeException
/** 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

View File

@ -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))
}
}

View File

@ -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)