diff --git a/compiler/src/dotty/tools/MainGenericRunner.scala b/compiler/src/dotty/tools/MainGenericRunner.scala index af23cf08728d..fa6faa7d570e 100644 --- a/compiler/src/dotty/tools/MainGenericRunner.scala +++ b/compiler/src/dotty/tools/MainGenericRunner.scala @@ -16,6 +16,9 @@ import java.util.jar._ import java.util.jar.Attributes.Name import dotty.tools.io.Jar import dotty.tools.runner.ScalaClassLoader +import java.nio.file.{Files, Paths, Path} +import scala.collection.JavaConverters._ +import dotty.tools.dotc.config.CommandLineParser enum ExecuteMode: case Guess @@ -102,7 +105,18 @@ object MainGenericRunner { case "-run" :: fqName :: tail => process(tail, settings.withExecuteMode(ExecuteMode.Run).withTargetToRun(fqName)) case ("-cp" | "-classpath" | "--class-path") :: cp :: tail => - process(tail, settings.copy(classPath = settings.classPath.appended(cp))) + val globdir = cp.replaceAll("[\\/][^\\/]*$", "") // slash/backslash agnostic + val (tailargs, cpstr) = if globdir.nonEmpty && classpathSeparator != ";" || cp.contains(classpathSeparator) then + (tail, cp) + else + // combine globbed classpath entries into a classpath + val jarfiles = cp :: tail + val cpfiles = jarfiles.takeWhile( f => f.startsWith(globdir) && ((f.toLowerCase.endsWith(".jar") || f.endsWith(".zip"))) ) + val tailargs = jarfiles.drop(cpfiles.size) + (tailargs, cpfiles.mkString(classpathSeparator)) + + process(tailargs, settings.copy(classPath = settings.classPath ++ cpstr.split(classpathSeparator).filter(_.nonEmpty))) + case ("-version" | "--version") :: _ => settings.copy( executeMode = ExecuteMode.Repl, @@ -123,7 +137,8 @@ object MainGenericRunner { case (o @ javaOption(striped)) :: tail => process(tail, settings.withJavaArgs(striped).withScalaArgs(o)) case (o @ scalaOption(_*)) :: tail => - process(tail, settings.withScalaArgs(o)) + val remainingArgs = (CommandLineParser.expandArg(o) ++ tail).toList + process(remainingArgs, settings) case (o @ colorOption(_*)) :: tail => process(tail, settings.withScalaArgs(o)) case arg :: tail => @@ -143,6 +158,13 @@ object MainGenericRunner { val settings = process(allArgs.toList, Settings()) if settings.exitCode != 0 then System.exit(settings.exitCode) + def removeCompiler(cp: Array[String]) = + if (!settings.compiler) then // Let's remove compiler from the classpath + val compilerLibs = Seq("scala3-compiler", "scala3-interfaces", "tasty-core", "scala-asm", "scala3-staging", "scala3-tasty-inspector") + cp.filterNot(c => compilerLibs.exists(c.contains)) + else + cp + def run(settings: Settings): Unit = settings.executeMode match case ExecuteMode.Repl => val properArgs = @@ -151,7 +173,7 @@ object MainGenericRunner { repl.Main.main(properArgs.toArray) case ExecuteMode.PossibleRun => - val newClasspath = (settings.classPath :+ ".").map(File(_).toURI.toURL) + val newClasspath = (settings.classPath :+ ".").flatMap(_.split(classpathSeparator).filter(_.nonEmpty)).map(File(_).toURI.toURL) import dotty.tools.runner.RichClassLoader._ val newClassLoader = ScalaClassLoader.fromURLsParallelCapable(newClasspath) val targetToRun = settings.possibleEntryPaths.to(LazyList).find { entryPath => @@ -166,15 +188,7 @@ object MainGenericRunner { run(settings.withExecuteMode(ExecuteMode.Repl)) case ExecuteMode.Run => val scalaClasspath = ClasspathFromClassloader(Thread.currentThread().getContextClassLoader).split(classpathSeparator) - - def removeCompiler(cp: Array[String]) = - if (!settings.compiler) then // Let's remove compiler from the classpath - val compilerLibs = Seq("scala3-compiler", "scala3-interfaces", "tasty-core", "scala-asm", "scala3-staging", "scala3-tasty-inspector") - cp.filterNot(c => compilerLibs.exists(c.contains)) - else - cp - val newClasspath = (settings.classPath ++ removeCompiler(scalaClasspath) :+ ".").map(File(_).toURI.toURL) - + val newClasspath = (settings.classPath.flatMap(_.split(classpathSeparator).filter(_.nonEmpty)) ++ removeCompiler(scalaClasspath) :+ ".").map(File(_).toURI.toURL) val res = ObjectRunner.runAndCatch(newClasspath, settings.targetToRun, settings.residualArgs).flatMap { case ex: ClassNotFoundException if ex.getMessage == settings.targetToRun => val file = settings.targetToRun @@ -187,14 +201,30 @@ object MainGenericRunner { } errorFn("", res) case ExecuteMode.Script => - val properArgs = - List("-classpath", settings.classPath.mkString(classpathSeparator)).filter(Function.const(settings.classPath.nonEmpty)) - ++ settings.residualArgs - ++ (if settings.save then List("-save") else Nil) - ++ List("-script", settings.targetScript) - ++ settings.scalaArgs - ++ settings.scriptArgs - scripting.Main.main(properArgs.toArray) + val targetScript = Paths.get(settings.targetScript).toFile + val targetJar = settings.targetScript.replaceAll("[.][^\\/]*$", "")+".jar" + val precompiledJar = Paths.get(targetJar).toFile + def mainClass = Jar(targetJar).mainClass.getOrElse("") // throws exception if file not found + val jarIsValid = precompiledJar.isFile && mainClass.nonEmpty && precompiledJar.lastModified >= targetScript.lastModified + if jarIsValid then + // precompiledJar exists, is newer than targetScript, and manifest defines a mainClass + sys.props("script.path") = targetScript.toPath.toAbsolutePath.normalize.toString + val scalaClasspath = ClasspathFromClassloader(Thread.currentThread().getContextClassLoader).split(classpathSeparator) + val newClasspath = (settings.classPath.flatMap(_.split(classpathSeparator).filter(_.nonEmpty)) ++ removeCompiler(scalaClasspath) :+ ".").map(File(_).toURI.toURL) + val mc = mainClass + if mc.nonEmpty then + ObjectRunner.runAndCatch(newClasspath :+ File(targetJar).toURI.toURL, mc, settings.scriptArgs) + else + Some(IllegalArgumentException(s"No main class defined in manifest in jar: $precompiledJar")) + else + val properArgs = + List("-classpath", settings.classPath.mkString(classpathSeparator)).filter(Function.const(settings.classPath.nonEmpty)) + ++ settings.residualArgs + ++ (if settings.save then List("-save") else Nil) + ++ settings.scalaArgs + ++ List("-script", settings.targetScript) + ++ settings.scriptArgs + scripting.Main.main(properArgs.toArray) case ExecuteMode.Guess => if settings.modeShouldBePossibleRun then run(settings.withExecuteMode(ExecuteMode.PossibleRun)) diff --git a/compiler/src/dotty/tools/dotc/config/CliCommand.scala b/compiler/src/dotty/tools/dotc/config/CliCommand.scala index 01ec346000c1..1e61914a89ca 100644 --- a/compiler/src/dotty/tools/dotc/config/CliCommand.scala +++ b/compiler/src/dotty/tools/dotc/config/CliCommand.scala @@ -7,8 +7,7 @@ import Settings._ import core.Contexts._ import Properties._ - import scala.PartialFunction.cond - import scala.collection.JavaConverters._ +import scala.PartialFunction.cond trait CliCommand: @@ -42,24 +41,10 @@ trait CliCommand: /** Distill arguments into summary detailing settings, errors and files to main */ def distill(args: Array[String], sg: Settings.SettingGroup)(ss: SettingsState = sg.defaultState)(using Context): ArgsSummary = - /** - * Expands all arguments starting with @ to the contents of the - * file named like each argument. - */ - def expandArg(arg: String): List[String] = - def stripComment(s: String) = s takeWhile (_ != '#') - val path = Paths.get(arg stripPrefix "@") - if (!Files.exists(path)) - report.error(s"Argument file ${path.getFileName} could not be found") - Nil - else - val lines = Files.readAllLines(path) // default to UTF-8 encoding - val params = lines.asScala map stripComment mkString " " - CommandLineParser.tokenize(params) // expand out @filename to the contents of that filename def expandedArguments = args.toList flatMap { - case x if x startsWith "@" => expandArg(x) + case x if x startsWith "@" => CommandLineParser.expandArg(x) case x => List(x) } diff --git a/compiler/src/dotty/tools/dotc/config/CommandLineParser.scala b/compiler/src/dotty/tools/dotc/config/CommandLineParser.scala index e3ca896d18d2..91017f7fc0c0 100644 --- a/compiler/src/dotty/tools/dotc/config/CommandLineParser.scala +++ b/compiler/src/dotty/tools/dotc/config/CommandLineParser.scala @@ -3,6 +3,8 @@ package dotty.tools.dotc.config import scala.annotation.tailrec import scala.collection.mutable.ArrayBuffer import java.lang.Character.isWhitespace +import java.nio.file.{Files, Paths} +import scala.collection.JavaConverters._ /** A simple enough command line parser. */ @@ -93,4 +95,19 @@ object CommandLineParser: def tokenize(line: String): List[String] = tokenize(line, x => throw new ParseException(x)) + /** + * Expands all arguments starting with @ to the contents of the + * file named like each argument. + */ + def expandArg(arg: String): List[String] = + def stripComment(s: String) = s takeWhile (_ != '#') + val path = Paths.get(arg stripPrefix "@") + if (!Files.exists(path)) + System.err.println(s"Argument file ${path.getFileName} could not be found") + Nil + else + val lines = Files.readAllLines(path) // default to UTF-8 encoding + val params = lines.asScala map stripComment mkString " " + tokenize(params) + class ParseException(msg: String) extends RuntimeException(msg) diff --git a/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala b/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala index e932ac54089a..2e616eeea631 100644 --- a/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala +++ b/compiler/test-coursier/dotty/tools/coursier/CoursierScalaTests.scala @@ -122,6 +122,16 @@ class CoursierScalaTests: assertTrue(output.mkString("\n").contains("Unable to create a system terminal")) // Scala attempted to create REPL so we can assume it is working replWithArgs() + def argumentFile() = + // verify that an arguments file is accepted + // verify that setting a user classpath does not remove compiler libraries from the classpath. + // arguments file contains "-classpath .", adding current directory to classpath. + val source = new File(getClass.getResource("/run/myfile.scala").getPath) + val argsFile = new File(getClass.getResource("/run/myargs.txt").getPath) + val output = CoursierScalaTests.csScalaCmd(s"@$argsFile", source.absPath) + assertEquals(output.mkString("\n"), "Hello") + argumentFile() + object CoursierScalaTests: def execCmd(command: String, options: String*): List[String] = diff --git a/compiler/test-coursier/run/myargs.txt b/compiler/test-coursier/run/myargs.txt new file mode 100755 index 000000000000..a0d2d24986de --- /dev/null +++ b/compiler/test-coursier/run/myargs.txt @@ -0,0 +1 @@ +-classpath . diff --git a/compiler/test-resources/scripting/argfileClasspath.sc b/compiler/test-resources/scripting/argfileClasspath.sc new file mode 100755 index 000000000000..c31371ba8934 --- /dev/null +++ b/compiler/test-resources/scripting/argfileClasspath.sc @@ -0,0 +1,9 @@ +#!dist/target/pack/bin/scala @compiler/test-resources/scripting/cpArgumentsFile.txt + +import java.nio.file.Paths + +def main(args: Array[String]): Unit = + val cwd = Paths.get(".").toAbsolutePath.toString.replace('\\', '/').replaceAll("/$", "") + printf("cwd: %s\n", cwd) + printf("classpath: %s\n", sys.props("java.class.path")) + diff --git a/compiler/test-resources/scripting/classpathReport.sc b/compiler/test-resources/scripting/classpathReport.sc index 5ccfd6faca76..e7b2f7067a1f 100755 --- a/compiler/test-resources/scripting/classpathReport.sc +++ b/compiler/test-resources/scripting/classpathReport.sc @@ -1,9 +1,12 @@ -#!dist/target/pack/bin/scala -classpath 'dist/target/pack/lib/*' +#!dist/target/pack/bin/scala -classpath dist/target/pack/lib/* import java.nio.file.Paths def main(args: Array[String]): Unit = - val cwd = Paths.get(".").toAbsolutePath.toString.replace('\\', '/').replaceAll("/$", "") + val cwd = Paths.get(".").toAbsolutePath.normalize.toString.norm printf("cwd: %s\n", cwd) - printf("classpath: %s\n", sys.props("java.class.path")) + printf("classpath: %s\n", sys.props("java.class.path").norm) + +extension(s: String) + def norm: String = s.replace('\\', '/') diff --git a/compiler/test-resources/scripting/cpArgumentsFile.txt b/compiler/test-resources/scripting/cpArgumentsFile.txt new file mode 100755 index 000000000000..73037eb7d9bc --- /dev/null +++ b/compiler/test-resources/scripting/cpArgumentsFile.txt @@ -0,0 +1 @@ +-classpath dist/target/pack/lib/* diff --git a/compiler/test-resources/scripting/scriptPath.sc b/compiler/test-resources/scripting/scriptPath.sc index a73064431319..46cd5e8a7385 100755 --- a/compiler/test-resources/scripting/scriptPath.sc +++ b/compiler/test-resources/scripting/scriptPath.sc @@ -1,4 +1,4 @@ -#!/usr/bin/env scala +#!dist/target/pack/bin/scala def main(args: Array[String]): Unit = args.zipWithIndex.foreach { case (arg,i) => printf("arg %d: [%s]\n",i,arg) } @@ -17,3 +17,6 @@ System.err.printf("sun.java.command: %s\n", sys.props("sun.java.command")) System.err.printf("first 5 PATH entries:\n%s\n",pathEntries.take(5).mkString("\n")) } + + extension(s: String) + def norm: String = s.replace('\\', '/') diff --git a/compiler/test/dotty/tools/scripting/BashScriptsTests.scala b/compiler/test/dotty/tools/scripting/BashScriptsTests.scala index a4c00ee7af49..8a6c6aad1ec1 100644 --- a/compiler/test/dotty/tools/scripting/BashScriptsTests.scala +++ b/compiler/test/dotty/tools/scripting/BashScriptsTests.scala @@ -58,7 +58,7 @@ class BashScriptsTests: /* verify `dist/bin/scala` non-interference with command line args following script name */ @Test def verifyScalaArgs = - val commandline = (Seq(scalaPath, showArgsScript) ++ testScriptArgs).mkString(" ") + val commandline = (Seq("SCALA_OPTS= ", scalaPath, showArgsScript) ++ testScriptArgs).mkString(" ") val (validTest, exitCode, stdout, stderr) = bashCommand(commandline) if validTest then var fail = false @@ -78,7 +78,7 @@ class BashScriptsTests: */ @Test def verifyScriptPathProperty = val scriptFile = testFiles.find(_.getName == "scriptPath.sc").get - val expected = s"/${scriptFile.getName}" + val expected = s"${scriptFile.getName}" printf("===> verify valid system property script.path is reported by script [%s]\n", scriptFile.getName) printf("calling scriptFile: %s\n", scriptFile) val (validTest, exitCode, stdout, stderr) = bashCommand(scriptFile.absPath) @@ -100,11 +100,12 @@ class BashScriptsTests: if validTest then val expected = s"${workingDirectory.toString}" val List(line1: String, line2: String) = stdout.take(2) + printf("line1 [%s]\n", line1) val valid = line2.dropWhile( _ != ' ').trim.startsWith(expected) if valid then printf(s"\n===> success: classpath begins with %s, as reported by [%s]\n", workingDirectory, scriptFile.getName) assert(valid, s"script ${scriptFile.absPath} did not report valid java.class.path first entry") - def existingPath: String = envOrElse("PATH","").norm + def existingPath: String = envOrElse("PATH", "").norm def adjustedPath = s"$javaHome/bin$psep$scalaHome/bin$psep$existingPath" def pathEntries = adjustedPath.split(psep).toList @@ -114,12 +115,12 @@ class BashScriptsTests: val path = Files.createTempFile("scriptingTest", ".args") val text = s"-classpath ${workingDirectory.absPath}" Files.write(path, text.getBytes(utfCharset)) - path.toFile.getAbsolutePath.replace('\\', '/') + path.toFile.getAbsolutePath.norm def fixHome(s: String): String = s.startsWith("~") match { case false => s - case true => s.replaceFirst("~",userHome) + case true => s.replaceFirst("~", userHome) } extension(s: String) { @@ -145,7 +146,7 @@ class BashScriptsTests: def absPath: String = f.getAbsolutePath.norm } - lazy val psep: String = propOrElse("path.separator","") + lazy val psep: String = propOrElse("path.separator", "") lazy val osname = propOrElse("os.name", "").toLowerCase lazy val scalacPath = s"$workingDirectory/dist/target/pack/bin/scalac".norm @@ -162,7 +163,7 @@ class BashScriptsTests: // else, SCALA_HOME if defined // else, not defined lazy val scalaHome = - if scalacPath.isFile then scalacPath.replaceAll("/bin/scalac","") + if scalacPath.isFile then scalacPath.replaceAll("/bin/scalac", "") else envOrElse("SCALA_HOME", "").norm lazy val javaHome = envOrElse("JAVA_HOME", "").norm @@ -171,7 +172,7 @@ class BashScriptsTests: ("JAVA_HOME", javaHome), ("SCALA_HOME", scalaHome), ("PATH", adjustedPath), - ).filter { case (name,valu) => valu.nonEmpty } + ).filter { case (name, valu) => valu.nonEmpty } lazy val whichBash: String = var whichBash = "" @@ -182,7 +183,7 @@ class BashScriptsTests: whichBash - def bashCommand(cmdstr: String, additionalEnvPairs:List[(String, String)] = Nil): (Boolean, Int, Seq[String], Seq[String]) = { + def bashCommand(cmdstr: String, additionalEnvPairs: List[(String, String)] = Nil): (Boolean, Int, Seq[String], Seq[String]) = { var (stdout, stderr) = (List.empty[String], List.empty[String]) if bashExe.toFile.exists then val cmd = Seq(bashExe, "-c", cmdstr) diff --git a/compiler/test/dotty/tools/scripting/ClasspathTests.scala b/compiler/test/dotty/tools/scripting/ClasspathTests.scala index 7ef0f9ee3fb3..159605cd967c 100755 --- a/compiler/test/dotty/tools/scripting/ClasspathTests.scala +++ b/compiler/test/dotty/tools/scripting/ClasspathTests.scala @@ -13,104 +13,29 @@ import scala.sys.process._ import scala.jdk.CollectionConverters._ import dotty.tools.dotc.config.Properties._ -/** Runs all tests contained in `compiler/test-resources/scripting/` */ +/** Test java command line generated by bin/scala and bin/scalac */ class ClasspathTests: val packBinDir = "dist/target/pack/bin" - val scalaCopy = makeTestableScriptCopy("scala") - val scalacCopy = makeTestableScriptCopy("scalac") - val commonCopy = makeTestableScriptCopy("common") + val packLibDir = "dist/target/pack/lib" // only interested in classpath test scripts - def testFiles = scripts("/scripting").filter { _.getName.matches("classpath.*[.]sc") } val testScriptName = "classpathReport.sc" - def testScript = testFiles.find { _.getName == testScriptName } match + val testScript = scripts("/scripting").find { _.getName.matches(testScriptName) } match case None => sys.error(s"test script not found: ${testScriptName}") case Some(file) => file - def getScriptPath(scriptName: String): Path = Paths.get(s"$packBinDir/$scriptName") - def exists(scriptPath: Path): Boolean = Files.exists(scriptPath) def packBinScalaExists:Boolean = exists(Paths.get(s"$packBinDir/scala")) - // create edited copy of [dist/bin/scala] and [dist/bin/scalac] for scalacEchoTest - def makeTestableScriptCopy(scriptName: String): Path = - val scriptPath: Path = getScriptPath(scriptName) - val scriptCopy: Path = getScriptPath(s"$scriptName-copy") - if Files.exists(scriptPath) then - val lines = Files.readAllLines(scriptPath).asScala.map { - _.replaceAll("/scalac", "/scalac-copy"). - replaceAll("/common", "/common-copy"). - replaceFirst("^ *eval(.*JAVACMD.*)", "echo $1") - } - val bytes = (lines.mkString("\n")+"\n").getBytes - Files.write(scriptCopy, bytes) - - scriptCopy - - /* - * verify java command line generated by scalac. - */ - @Test def scalacEchoTest = - val relpath = testScript.toPath.relpath.norm - printf("===> scalacEchoTest for script [%s]\n", relpath) - printf("bash is [%s]\n", bashExe) - - if packBinScalaExists then - val bashCmdline = s"SCALA_OPTS= ${scalaCopy.norm} -classpath '$wildcardEntry' $relpath" - - // ask [dist/bin/scalac] to echo generated command line so we can verify some things - val cmd = Array(bashExe, "-c", bashCmdline) - - //cmd.foreach { printf("[%s]\n", _) } - - val javaCommandLine = exec(cmd:_*).mkString(" ").split(" ").filter { _.trim.nonEmpty } - printf("\n==================== isWin[%s], cygwin[%s], mingw[%s], msys[%s]\n", isWin, cygwin, mingw, msys) - javaCommandLine.foreach { printf("java-command[%s]\n", _) } - - val output = scala.collection.mutable.Queue(javaCommandLine:_*) - output.dequeueWhile( _ != "dotty.tools.scripting.Main") - - def consumeNext = if output.isEmpty then "" else output.dequeue() - - // assert that we found "dotty.tools.scripting.Main" - val str = consumeNext - if str != "dotty.tools.scripting.Main" then - - assert(str == "dotty.tools.scripting.Main", s"found [$str]") - val mainArgs = output.copyToArray(Array.ofDim[String](output.length)) - - // display command line starting with "dotty.tools.scripting.Main" - output.foreach { line => - printf("%s\n", line) - } - - // expecting -classpath next - assert(consumeNext.replaceAll("'", "") == "-classpath") - - // 2nd arg to scripting.Main is 'lib/*', with semicolon added if Windows jdk - - // PR #10761: verify that [dist/bin/scala] -classpath processing adds $psep to wildcard if Windows - val classpathValue = consumeNext - printf("classpath value [%s]\n", classpathValue) - assert( !winshell || classpathValue.contains(psep) ) - - // expecting -script next - assert(consumeNext.replaceAll("'", "") == "-script") - - // PR #10761: verify that Windows jdk did not expand single wildcard classpath to multiple file paths - if javaCommandLine.last != relpath then - printf("last: %s\nrelp: %s\n", javaCommandLine.last, relpath) - assert(javaCommandLine.last == relpath, s"unexpected output passed to scripting.Main") - /* * verify classpath reported by called script. */ - @Test def hashbangClasspathVerifyTest = + @Test def hashbangClasspathVerifyTest = { val relpath = testScript.toPath.relpath.norm printf("===> hashbangClasspathVerifyTest for script [%s]\n", relpath) printf("bash is [%s]\n", bashExe) - if false && packBinScalaExists then + if packBinScalaExists then val bashCmdline = s"SCALA_OPTS= $relpath" val cmd = Array(bashExe, "-c", bashCmdline) @@ -123,7 +48,7 @@ class ClasspathTests: val scriptCp = findTaggedLine("classpath", scriptOutput) val hashbangClasspathJars = scriptCp.split(psep).map { _.getName }.sorted.distinct - val packlibJars = listJars(s"$scriptCwd/dist/target/pack/lib").sorted.distinct + val packlibJars = listJars(s"$scriptCwd/$packLibDir").sorted.distinct // verify that the classpath set in the hashbang line is effective if hashbangClasspathJars.size != packlibJars.size then @@ -131,6 +56,7 @@ class ClasspathTests: printf("%d jar files in dist/target/pack/lib\n", packlibJars.size) assert(hashbangClasspathJars.size == packlibJars.size) + } //////////////// end of tests //////////////// @@ -150,7 +76,9 @@ lazy val env:Map[String, String] = System.getenv.asScala.toMap // script output expected as ": " def findTaggedLine(tag: String, lines: Seq[String]): String = lines.find { _.startsWith(tag) } match - case None => sys.error(s"no $tag: found in script output") + case None => + lines.foreach { System.err.printf("line[%s]\n", _) } + sys.error(s"no $tag: found in script output") case Some(cwd) => cwd.dropWhile( _ != ' ').trim // discard tag def exec(cmd: String *): Seq[String] = Process(cmd).lazyLines_!.toList