diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index d504809a9a2e..1489a2e74d95 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -273,7 +273,7 @@ object Contexts { /** Sourcefile corresponding to given abstract file, memoized */ def getSource(file: AbstractFile, codec: => Codec = Codec(settings.encoding.value)) = { util.Stats.record("Context.getSource") - base.sources.getOrElseUpdate(file, new SourceFile(file, codec)) + base.sources.getOrElseUpdate(file, SourceFile(file, codec)) } /** SourceFile with given path name, memoized */ diff --git a/compiler/src/dotty/tools/dotc/util/SourceFile.scala b/compiler/src/dotty/tools/dotc/util/SourceFile.scala index fcd8b2d55d06..9f555638a40e 100644 --- a/compiler/src/dotty/tools/dotc/util/SourceFile.scala +++ b/compiler/src/dotty/tools/dotc/util/SourceFile.scala @@ -22,19 +22,34 @@ object ScriptSourceFile { @sharable private val headerPattern = Pattern.compile("""^(::)?!#.*(\r|\n|\r\n)""", Pattern.MULTILINE) private val headerStarts = List("#!", "::#!") + /** Return true if has a script header */ + def hasScriptHeader(content: Array[Char]): Boolean = + headerStarts.exists(content.startsWith(_)) + def apply(file: AbstractFile, content: Array[Char]): SourceFile = { /** Length of the script header from the given content, if there is one. - * The header begins with "#!" or "::#!" and ends with a line starting - * with "!#" or "::!#". + * The header begins with "#!" or "::#!" and is either a single line, + * or it ends with a line starting with "!#" or "::!#", if present. */ val headerLength = if (headerStarts exists (content startsWith _)) { val matcher = headerPattern matcher content.mkString if (matcher.find) matcher.end - else throw new IOException("script file does not close its header with !# or ::!#") + else content.indexOf('\n') // end of first line } else 0 - new SourceFile(file, content drop headerLength) { + + // overwrite hash-bang lines with all spaces to preserve line numbers + val hashBangLines = content.take(headerLength).mkString.split("\\r?\\n") + if hashBangLines.nonEmpty then + for i <- 0 until headerLength do + content(i) match { + case '\r' | '\n' => + case _ => + content(i) = ' ' + } + + new SourceFile(file, content) { override val underlying = new SourceFile(this.file, this.content) } } @@ -245,6 +260,25 @@ object SourceFile { else sourcePath.toString } + + /** Return true if file is a script: + * if filename extension is not .scala and has a script header. + */ + def isScript(file: AbstractFile, content: Array[Char]): Boolean = + ScriptSourceFile.hasScriptHeader(content) + + def apply(file: AbstractFile, codec: Codec): SourceFile = + // see note above re: Files.exists is remarkably slow + val chars = + try + new String(file.toByteArray, codec.charSet).toCharArray + catch + case _: java.nio.file.NoSuchFileException => Array[Char]() + + if isScript(file, chars) then + ScriptSourceFile(file, chars) + else + new SourceFile(file, chars) } @sharable object NoSource extends SourceFile(NoAbstractFile, Array[Char]()) { diff --git a/compiler/src/dotty/tools/scripting/Main.scala b/compiler/src/dotty/tools/scripting/Main.scala index f820421860a6..403af7b46435 100644 --- a/compiler/src/dotty/tools/scripting/Main.scala +++ b/compiler/src/dotty/tools/scripting/Main.scala @@ -1,22 +1,108 @@ package dotty.tools.scripting import java.io.File +import java.nio.file.Path +import dotty.tools.dotc.config.Properties.isWin /** Main entry point to the Scripting execution engine */ object Main: /** All arguments before -script are compiler arguments. All arguments afterwards are script arguments.*/ - def distinguishArgs(args: Array[String]): (Array[String], File, Array[String]) = - val (compilerArgs, rest) = args.splitAt(args.indexOf("-script")) + private def distinguishArgs(args: Array[String]): (Array[String], File, Array[String], Boolean, Boolean) = + val (leftArgs, rest) = args.splitAt(args.indexOf("-script")) + assert(rest.size >= 2,s"internal error: rest == Array(${rest.mkString(",")})") + val file = File(rest(1)) val scriptArgs = rest.drop(2) - (compilerArgs, file, scriptArgs) + var saveJar = false + var invokeFlag = true // by default, script main method is invoked + val compilerArgs = leftArgs.filter { + case "-save" | "-savecompiled" => + saveJar = true + false + case "-compile-only" => + invokeFlag = false // no call to script main method + false + case _ => + true + } + (compilerArgs, file, scriptArgs, saveJar, invokeFlag) end distinguishArgs def main(args: Array[String]): Unit = - val (compilerArgs, scriptFile, scriptArgs) = distinguishArgs(args) - try ScriptingDriver(compilerArgs, scriptFile, scriptArgs).compileAndRun() + val (compilerArgs, scriptFile, scriptArgs, saveJar, invokeFlag) = distinguishArgs(args) + val driver = ScriptingDriver(compilerArgs, scriptFile, scriptArgs) + try driver.compileAndRun { (outDir:Path, classpath:String, mainClass: String) => + if saveJar then + // write a standalone jar to the script parent directory + writeJarfile(outDir, scriptFile, scriptArgs, classpath, mainClass) + invokeFlag + } catch case ScriptingException(msg) => println(s"Error: $msg") sys.exit(1) + + case e: java.lang.reflect.InvocationTargetException => + throw e.getCause + + private def writeJarfile(outDir: Path, scriptFile: File, scriptArgs:Array[String], + classpath:String, mainClassName: String): Unit = + + val javaClasspath = sys.props("java.class.path") + val runtimeClasspath = s"${classpath}$pathsep$javaClasspath" + + val jarTargetDir: Path = Option(scriptFile.toPath.getParent) match { + case None => sys.error(s"no parent directory for script file [$scriptFile]") + case Some(parent) => parent + } + + def scriptBasename = scriptFile.getName.takeWhile(_!='.') + val jarPath = s"$jarTargetDir/$scriptBasename.jar" + + val cpPaths = runtimeClasspath.split(pathsep).map(_.absPath) + + import java.util.jar.Attributes.Name + val cpString:String = cpPaths.distinct.mkString(" ") + val manifestAttributes:Seq[(Name, String)] = Seq( + (Name.MANIFEST_VERSION, "1.0"), + (Name.MAIN_CLASS, mainClassName), + (Name.CLASS_PATH, cpString), + ) + import dotty.tools.io.{Jar, Directory} + val jar = new Jar(jarPath) + val writer = jar.jarWriter(manifestAttributes:_*) + try + writer.writeAllFrom(Directory(outDir)) + finally + writer.close() + end writeJarfile + + def pathsep = sys.props("path.separator") + + + extension(file: File){ + def norm: String = file.toString.norm + } + + extension(path: String) { + // Normalize path separator, convert relative path to absolute + def norm: String = + path.replace('\\', '/') match { + case s if s.secondChar == ":" => s.drop(2) + case s if s.startsWith("./") => s.drop(2) + case s => s + } + + // convert to absolute path relative to cwd. + def absPath: String = norm match + case str if str.isAbsolute => norm + case _ => s"/${sys.props("user.dir").norm}/$norm" + + def absFile: File = File(path.absPath) + + // Treat norm paths with a leading '/' as absolute. + // Windows java.io.File#isAbsolute treats them as relative. + def isAbsolute = path.norm.startsWith("/") || (isWin && path.secondChar == ":") + def secondChar: String = path.take(2).drop(1).mkString("") + } diff --git a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala index d5fad4ef520a..a23840b9f8e0 100644 --- a/compiler/src/dotty/tools/scripting/ScriptingDriver.scala +++ b/compiler/src/dotty/tools/scripting/ScriptingDriver.scala @@ -17,7 +17,7 @@ import dotty.tools.dotc.config.Settings.Setting._ import sys.process._ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: Array[String]) extends Driver: - def compileAndRun(): Unit = + def compileAndRun(pack:(Path, String, String) => Boolean = null): Unit = val outDir = Files.createTempDirectory("scala3-scripting") val (toCompile, rootCtx) = setup(compilerArgs :+ scriptFile.getAbsolutePath, initCtx.fresh) given Context = rootCtx.fresh.setSetting(rootCtx.settings.outputDir, @@ -26,7 +26,16 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: if doCompile(newCompiler, toCompile).hasErrors then throw ScriptingException("Errors encountered during compilation") - try detectMainMethod(outDir, ctx.settings.classpath.value).invoke(null, scriptArgs) + try + val (mainClass, mainMethod) = detectMainClassAndMethod(outDir, ctx.settings.classpath.value, scriptFile) + val invokeMain: Boolean = + Option(pack) match + case Some(func) => + func(outDir, ctx.settings.classpath.value, mainClass) + case None => + true + end match + if invokeMain then mainMethod.invoke(null, scriptArgs) catch case e: java.lang.reflect.InvocationTargetException => throw e.getCause @@ -41,12 +50,13 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: target.delete() end deleteFile - private def detectMainMethod(outDir: Path, classpath: String): Method = + private def detectMainClassAndMethod(outDir: Path, classpath: String, + scriptFile: File): (String, Method) = val outDirURL = outDir.toUri.toURL - val classpathUrls = classpath.split(":").map(File(_).toURI.toURL) + val classpathUrls = classpath.split(pathsep).map(File(_).toURI.toURL) val cl = URLClassLoader(classpathUrls :+ outDirURL) - def collectMainMethods(target: File, path: String): List[Method] = + def collectMainMethods(target: File, path: String): List[(String, Method)] = val nameWithoutExtension = target.getName.takeWhile(_ != '.') val targetPath = if path.nonEmpty then s"${path}.${nameWithoutExtension}" @@ -61,7 +71,7 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: val cls = cl.loadClass(targetPath) try val method = cls.getMethod("main", classOf[Array[String]]) - if Modifier.isStatic(method.getModifiers) then List(method) else Nil + if Modifier.isStatic(method.getModifiers) then List((cls.getName, method)) else Nil catch case _: java.lang.NoSuchMethodException => Nil else Nil @@ -74,13 +84,16 @@ class ScriptingDriver(compilerArgs: Array[String], scriptFile: File, scriptArgs: candidates match case Nil => - throw ScriptingException("No main methods detected in your script") + throw ScriptingException(s"No main methods detected in script ${scriptFile}") case _ :: _ :: _ => throw ScriptingException("A script must contain only one main method. " + s"Detected the following main methods:\n${candidates.mkString("\n")}") case m :: Nil => m end match - end detectMainMethod + end detectMainClassAndMethod + + def pathsep = sys.props("path.separator") + end ScriptingDriver case class ScriptingException(msg: String) extends RuntimeException(msg) diff --git a/compiler/test-resources/scripting/hashBang.sc b/compiler/test-resources/scripting/hashBang.sc new file mode 100644 index 000000000000..d767bd1a1592 --- /dev/null +++ b/compiler/test-resources/scripting/hashBang.sc @@ -0,0 +1,25 @@ +#!/usr/bin/env scala +# comment +STUFF=nada +!# +// everything above this point should be ignored by the compiler +def main(args: Array[String]): Unit = + args.zipWithIndex.foreach { case (arg,i) => printf("arg %d: [%s]\n",i,arg) } + System.err.printf("mainClassFromStack: %s\n",mainFromStack) + assert(mainFromStack.contains("hashBang"),s"fromStack[$mainFromStack]") + + lazy val mainFromStack:String = { + val result = new java.io.StringWriter() + new RuntimeException("stack").printStackTrace(new java.io.PrintWriter(result)) + val stack = result.toString.split("[\r\n]+").toList + if verbose then for( s <- stack ){ System.err.printf("[%s]\n",s) } + stack.filter { str => str.contains(".main(") }.map { + // derive main class name from stack when main object is NOT declared in source + _.replaceAll("[.].*",""). + replaceAll("\\s+at\\s+","") + }.distinct.take(1).mkString("") + } + + lazy val verbose = Option(System.getenv("DOTC_VERBOSE")) match + case None => false + case _ => true diff --git a/compiler/test-resources/scripting/mainClassOnStack.sc b/compiler/test-resources/scripting/mainClassOnStack.sc new file mode 100644 index 000000000000..a0c974555e76 --- /dev/null +++ b/compiler/test-resources/scripting/mainClassOnStack.sc @@ -0,0 +1,29 @@ +#!/usr/bin/env scala +export STUFF=nada +#lots of other stuff that isn't valid scala +!# +// everything above this point should be ignored by the compiler +object Zoo { + def main(args: Array[String]): Unit = + args.zipWithIndex.foreach { case (arg,i) => printf("arg %d: [%s]\n",i,arg) } + printf("mainClassFromStack: %s\n",mainClassFromStack) + assert(mainClassFromStack == "Zoo",s"fromStack[$mainClassFromStack]") + + lazy val mainClassFromStack:String = { + val result = new java.io.StringWriter() + new RuntimeException("stack").printStackTrace(new java.io.PrintWriter(result)) + val stack = result.toString.split("[\r\n]+").toList + if verbose then for( s <- stack ){ System.err.printf("[%s]\n",s) } + val shortStack = stack.filter { str => str.contains(".main(") && ! str.contains("$") }.map { + // derive main class name from stack when main object is declared in source + _.replaceAll("[.].*",""). + replaceAll("\\s+at\\s+","") + } + // for( s <- shortStack ){ System.err.printf("[%s]\n",s) } + shortStack.take(1).mkString("|") + } + + lazy val verbose = Option(System.getenv("DOTC_VERBOSE")) match + case None => false + case _ => true +} diff --git a/compiler/test-resources/scripting/scriptParent.scala b/compiler/test-resources/scripting/scriptParent.scala new file mode 100644 index 000000000000..709f8a19a09f --- /dev/null +++ b/compiler/test-resources/scripting/scriptParent.scala @@ -0,0 +1,14 @@ +import java.nio.file.Paths + +object ScriptParent { + def main(args: Array[String]): Unit = { + args.zipWithIndex.foreach { case (arg,i) => printf("arg %d: [%s]\n",i,arg) } + val scriptName = Option(sys.props("script.path")) match { + case None => + printf("no script.path property\n") + case Some(script) => + val p = Paths.get(script).toAbsolutePath.toFile.getParent + printf("parentDir: [%s]\n",p) + } + } +} diff --git a/compiler/test-resources/scripting/scriptPath.sc b/compiler/test-resources/scripting/scriptPath.sc new file mode 100644 index 000000000000..49ed65a76515 --- /dev/null +++ b/compiler/test-resources/scripting/scriptPath.sc @@ -0,0 +1,10 @@ +#!/usr/bin/env scala + + def main(args: Array[String]): Unit = + args.zipWithIndex.foreach { case (arg,i) => printf("arg %d: [%s]\n",i,arg) } + val path = Option(sys.props("script.path")) match { + case None => printf("no script.path property is defined\n") + case Some(path) => + printf("script.path: %s\n",path) + assert(path.endsWith("scriptPath.sc"),s"actual path [$path]") + } diff --git a/compiler/test-resources/scripting/touchFile.sc b/compiler/test-resources/scripting/touchFile.sc new file mode 100755 index 000000000000..974f8a64d192 --- /dev/null +++ b/compiler/test-resources/scripting/touchFile.sc @@ -0,0 +1,8 @@ +#!/usr/bin/env scala + +import java.io.File + +// create an empty file +def main(args: Array[String]): Unit = + val file = File("touchedFile.out") + file.createNewFile(); diff --git a/compiler/test/dotty/tools/scripting/ScriptingTests.scala b/compiler/test/dotty/tools/scripting/ScriptingTests.scala index 547f525e98e4..e7399c68f09a 100644 --- a/compiler/test/dotty/tools/scripting/ScriptingTests.scala +++ b/compiler/test/dotty/tools/scripting/ScriptingTests.scala @@ -9,31 +9,175 @@ import org.junit.Test import vulpix.TestConfiguration -/** Runs all tests contained in `compiler/test-resources/repl/` */ +/** Runs all tests contained in `compiler/test-resources/scripting/` */ class ScriptingTests: extension (str: String) def dropExtension = str.reverse.dropWhile(_ != '.').drop(1).reverse - @Test def scriptingTests = - val testFiles = scripts("/scripting") + extension(f: File) def absPath = + f.getAbsolutePath.replace('\\','/') - val argss: Map[String, Array[String]] = ( - for - argFile <- testFiles - if argFile.getName.endsWith(".args") - name = argFile.getName.dropExtension - scriptArgs = readLines(argFile).toArray - yield name -> scriptArgs).toMap + def testFiles = scripts("/scripting") + def script2jar(scriptFile: File) = + val jarName = s"${scriptFile.getName.dropExtension}.jar" + File(scriptFile.getParent,jarName) + + def showScriptUnderTest(scriptFile: File): Unit = + printf("===> test script name [%s]\n",scriptFile.getName) + + val argss: Map[String, Array[String]] = ( + for + argFile <- testFiles + if argFile.getName.endsWith(".args") + name = argFile.getName.dropExtension + scriptArgs = readLines(argFile).toArray + yield name -> scriptArgs).toMap + + def scalaFilesWithArgs(extension: String) = ( for scriptFile <- testFiles - if scriptFile.getName.endsWith(".scala") + if scriptFile.getName.endsWith(extension) name = scriptFile.getName.dropExtension scriptArgs = argss.getOrElse(name, Array.empty[String]) - do + yield scriptFile -> scriptArgs).toList.sortBy { (file,args) => file.getName } + + def callExecutableJar(script: File,jar: File, scriptArgs: Array[String] = Array.empty[String]) = { + import scala.sys.process._ + val cmd = Array("java",s"-Dscript.path=${script.getName}","-jar",jar.absPath) + ++ scriptArgs + Process(cmd).lazyLines_!.foreach { println } + } + + /* + * Call .scala scripts without -save option, verify no jar created + */ + @Test def scriptingDriverTests = + for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".scala") do + showScriptUnderTest(scriptFile) + val unexpectedJar = script2jar(scriptFile) + unexpectedJar.delete + + sys.props("script.path") = scriptFile.absPath ScriptingDriver( compilerArgs = Array( - "-classpath", TestConfiguration.basicClasspath), + "-classpath", TestConfiguration.basicClasspath + ), scriptFile = scriptFile, scriptArgs = scriptArgs - ).compileAndRun() + ).compileAndRun { (path:java.nio.file.Path,classpath:String, mainClass:String) => + printf("mainClass from ScriptingDriver: %s\n",mainClass) + true // call compiled script main method + } + assert(! unexpectedJar.exists, s"not expecting jar file: ${unexpectedJar.absPath}") + + /* + * Call .sc scripts without -save option, verify no jar created + */ + @Test def scriptingMainTests = + for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".sc") do + showScriptUnderTest(scriptFile) + val unexpectedJar = script2jar(scriptFile) + unexpectedJar.delete + + sys.props("script.path") = scriptFile.absPath + val mainArgs: Array[String] = Array( + "-classpath", TestConfiguration.basicClasspath.toString, + "-script", scriptFile.toString, + ) ++ scriptArgs + + Main.main(mainArgs) + assert(! unexpectedJar.exists, s"not expecting jar file: ${unexpectedJar.absPath}") + + /* + * Call .sc scripts with -save option, verify jar is created. + */ + @Test def scriptingJarTest = + for (scriptFile,scriptArgs) <- scalaFilesWithArgs(".sc") do + showScriptUnderTest(scriptFile) + val expectedJar = script2jar(scriptFile) + expectedJar.delete + + sys.props("script.path") = scriptFile.absPath + val mainArgs: Array[String] = Array( + "-classpath", TestConfiguration.basicClasspath.toString, + "-save", + "-script", scriptFile.toString, + ) ++ scriptArgs + + Main.main(mainArgs) + + printf("===> test script jar name [%s]\n",expectedJar.getName) + assert(expectedJar.exists) + + callExecutableJar(scriptFile, expectedJar, scriptArgs) + + /* + * Verify that when ScriptingDriver callback returns true, main is called. + * Verify that when ScriptingDriver callback returns false, main is not called. + */ + @Test def scriptCompileOnlyTests = + val scriptFile = touchFileScript + showScriptUnderTest(scriptFile) + + // verify main method not called when false is returned + printf("testing script compile, with no call to script main method.\n") + touchedFile.delete + assert(!touchedFile.exists, s"unable to delete ${touchedFile}") + ScriptingDriver( + compilerArgs = Array("-classpath", TestConfiguration.basicClasspath), + scriptFile = scriptFile, + scriptArgs = Array.empty[String] + ).compileAndRun { (path:java.nio.file.Path,classpath:String, mainClass:String) => + printf("success: no call to main method in mainClass: %s\n",mainClass) + false // no call to compiled script main method + } + touchedFile.delete + assert( !touchedFile.exists, s"unable to delete ${touchedFile}" ) + + // verify main method is called when true is returned + printf("testing script compile, with call to script main method.\n") + ScriptingDriver( + compilerArgs = Array("-classpath", TestConfiguration.basicClasspath), + scriptFile = scriptFile, + scriptArgs = Array.empty[String] + ).compileAndRun { (path:java.nio.file.Path,classpath:String, mainClass:String) => + printf("call main method in mainClass: %s\n",mainClass) + true // call compiled script main method, create touchedFile + } + + if touchedFile.exists then + printf("success: script created file %s\n",touchedFile) + if touchedFile.exists then printf("success: created file %s\n",touchedFile) + assert( touchedFile.exists, s"expected to find file ${touchedFile}" ) + + /* + * Compile touchFile.sc to create executable jar, verify jar execution succeeds. + */ + @Test def scriptingNoCompileJar = + val scriptFile = touchFileScript + showScriptUnderTest(scriptFile) + val expectedJar = script2jar(scriptFile) + sys.props("script.path") = scriptFile.absPath + val mainArgs: Array[String] = Array( + "-classpath", TestConfiguration.basicClasspath.toString, + "-save", + "-script", scriptFile.toString, + "-compile-only" + ) + + expectedJar.delete + Main.main(mainArgs) // create executable jar + printf("===> test script jar name [%s]\n",expectedJar.getName) + assert(expectedJar.exists,s"unable to create executable jar [$expectedJar]") + + touchedFile.delete + assert(!touchedFile.exists,s"unable to delete ${touchedFile}") + printf("calling executable jar %s\n",expectedJar) + callExecutableJar(scriptFile, expectedJar) + if touchedFile.exists then + printf("success: executable jar created file %s\n",touchedFile) + assert( touchedFile.exists, s"expected to find file ${touchedFile}" ) + + def touchFileScript = testFiles.find(_.getName == "touchFile.sc").get + def touchedFile = File("touchedFile.out") diff --git a/dist/bin/scala b/dist/bin/scala index 3c522a082d3b..ceb9fbf0e2df 100755 --- a/dist/bin/scala +++ b/dist/bin/scala @@ -45,6 +45,7 @@ execute_script=false with_compiler=false class_path_count=0 CLASS_PATH="" +save_compiled=false # Little hack to check if all arguments are options all_params="$*" @@ -52,6 +53,8 @@ truncated_params="${*#-}" # options_indicator != 0 if at least one parameter is not an option options_indicator=$(( ${#all_params} - ${#truncated_params} - $# )) +[ -n "$SCALA_OPTS" ] && set -- "$@" $SCALA_OPTS + while [[ $# -gt 0 ]]; do case "$1" in -repl) @@ -72,6 +75,15 @@ while [[ $# -gt 0 ]]; do with_compiler=true shift ;; + @*|-color:*|-compile-only) + addDotcOptions "${1}" + shift + ;; + -save|-savecompiled) + save_compiled=true + addDotcOptions "${1}" + shift + ;; -d) DEBUG="$DEBUG_STR" shift @@ -82,7 +94,8 @@ while [[ $# -gt 0 ]]; do shift ;; *) if [ $execute_script == false ]; then - if [[ "$1" == *.scala ]]; then + # is a script if extension .scala or .sc or if has scala hash bang + if [[ -e "$1" && ("$1" == *.scala || "$1" == *.sc || -f "$1" && `head -n 1 -- "$1" | grep '#!.*scala'`) ]]; then execute_script=true target_script="$1" else @@ -98,10 +111,19 @@ while [[ $# -gt 0 ]]; do done if [ $execute_script == true ]; then + [ -n "$script_trace" ] && set -x if [ "$CLASS_PATH" ]; then cp_arg="-classpath \"$CLASS_PATH\"" fi - eval "\"$PROG_HOME/bin/scalac\" $cp_arg ${java_options[@]} ${residual_args[@]} -script $target_script ${script_args[@]}" + setScriptName="-Dscript.path=$target_script" + target_jar="${target_script%.*}.jar" + if [[ $save_compiled == true && "$target_jar" -nt "$target_script" ]]; then + java $setScriptName -jar "$target_jar" "${script_args[@]}" + else + [[ $save_compiled == true ]] && rm -f $target_jar + residual_args+=($setScriptName) + eval "\"$PROG_HOME/bin/scalac\" $cp_arg ${java_options[@]} ${residual_args[@]} -script $target_script ${script_args[@]}" + fi elif [ $execute_repl == true ] || ([ $execute_run == false ] && [ $options_indicator == 0 ]); then if [ "$CLASS_PATH" ]; then cp_arg="-classpath \"$CLASS_PATH\""