diff --git a/src/main/scala/ru/spbau/mit/softwaredesign/cli/interpreter/CommandLineInterpreter.scala b/src/main/scala/ru/spbau/mit/softwaredesign/cli/interpreter/CommandLineInterpreter.scala index bcf12e0..48bdbfa 100644 --- a/src/main/scala/ru/spbau/mit/softwaredesign/cli/interpreter/CommandLineInterpreter.scala +++ b/src/main/scala/ru/spbau/mit/softwaredesign/cli/interpreter/CommandLineInterpreter.scala @@ -1,6 +1,7 @@ package ru.spbau.mit.softwaredesign.cli.interpreter -import java.io._ +import java.io.{File, _} +import java.net.URLDecoder import java.nio.channels._ import org.apache.commons.io.IOUtils @@ -15,6 +16,7 @@ import scala.util.Properties /** Stores environment and performs command interpretation */ class CommandLineInterpreter { + private var workDirectory = System.getProperty("user.dir") private val environment = mutable.Map[String, String]().withDefaultValue("") /** Evaluate a pipeline of commands */ @@ -45,6 +47,8 @@ class CommandLineInterpreter { case "wc" => evalWc(args, in, out) case "pwd" => evalPwd(out) case "grep" => evalGrep(args, in, out) + case "cd" => evalCd(args, out) + case "ls" => evalLs(args, out) case "exit" => evalExit() case commandName => evalExternal(commandName, args, in, out) } @@ -58,7 +62,7 @@ class CommandLineInterpreter { } else { args.foreach { arg => val fileName = processBlock(arg) - for (fileIn <- managed(new FileInputStream(fileName))) { + for (fileIn <- managed(new FileInputStream(getAbsolutePath(fileName)))) { IOUtils.copy(fileIn, out) } } @@ -66,7 +70,9 @@ class CommandLineInterpreter { } private def evalEcho(args: List[Block], out: OutputStream): Unit = { - val concatArgs = args.map { processBlock } mkString " " + val concatArgs = args.map { + processBlock + } mkString " " out.write((concatArgs + Properties.lineSeparator).getBytes) } @@ -85,7 +91,7 @@ class CommandLineInterpreter { } else { args.foreach { arg => val fileName = processBlock(arg) - for (fileIn <- managed(new FileInputStream(fileName))) { + for (fileIn <- managed(new FileInputStream(getAbsolutePath(fileName)))) { val source = Source.fromInputStream(fileIn).mkString val (lines, words, bytes) = countLinesWordsBytes(source) out.write(s"$lines $words $bytes $fileName${Properties.lineSeparator}".getBytes) @@ -95,8 +101,7 @@ class CommandLineInterpreter { } private def evalPwd(out: OutputStream): Unit = { - val userDir = System.getProperty("user.dir") - out.write((userDir + Properties.lineSeparator).getBytes) + out.write((workDirectory + Properties.lineSeparator).getBytes) } private def evalGrep(args: List[Block], in: InputStream, out: OutputStream): Unit = { @@ -109,23 +114,25 @@ class CommandLineInterpreter { val parser = new OptionParser[Config]("grep") { head("grep", "0.99") - opt[Unit]('i', "ignore-case").action( (_, config) => - config.copy(ignoreCase = true) ).text("Ignore case distinctions in both the and the input files.") + opt[Unit]('i', "ignore-case").action((_, config) => + config.copy(ignoreCase = true)).text("Ignore case distinctions in both the and the input files.") - opt[Unit]('w', "word_regexp").action( (_, config) => - config.copy(wordRegexp = true) ).text("Select only those lines containing matches that form whole words.") + opt[Unit]('w', "word_regexp").action((_, config) => + config.copy(wordRegexp = true)).text("Select only those lines containing matches that form whole words.") - opt[Int]('A', "after-context").action( (value, config) => - config.copy(afterContext = value) ).text("Print lines of trailing context after matching lines.") + opt[Int]('A', "after-context").action((value, config) => + config.copy(afterContext = value)).text("Print lines of trailing context after matching lines.") - arg[String]("").required().action( (regex, config) => - config.copy(pattern = regex) ).text("Use regex as the pattern.") + arg[String]("").required().action((regex, config) => + config.copy(pattern = regex)).text("Use regex as the pattern.") - arg[String]("...").unbounded().optional().action( (fileName, config) => - config.copy(fileNames = config.fileNames :+ fileName) ).text("Obtain patterns from files, one per line.") + arg[String]("...").unbounded().optional().action((fileName, config) => + config.copy(fileNames = config.fileNames :+ fileName)).text("Obtain patterns from files, one per line.") } - parser.parse(args.map { processBlock }, Config()) match { + parser.parse(args.map { + processBlock + }, Config()) match { case Some(Config(ignoreCase, wordRegexp, afterContext, pattern, fileNames)) => var regexBuilder = pattern if (ignoreCase) @@ -158,7 +165,7 @@ class CommandLineInterpreter { handleInput(in) } else { fileNames.foreach { fileName => - for (fileIn <- managed(new FileInputStream(fileName))) { + for (fileIn <- managed(new FileInputStream(getAbsolutePath(fileName)))) { handleInput(fileIn) } } @@ -167,12 +174,49 @@ class CommandLineInterpreter { } } + private def evalCd(args: List[Block], out: OutputStream): Unit = { + if (args.isEmpty) { + workDirectory = System.getProperty("user.home") + } else { + val dirName = processBlock(args.head) + val dir = new File(getAbsolutePath(dirName)) + if (dir.exists() && dir.isDirectory) { + workDirectory = dir.getCanonicalPath + } else { + throw new IOException(getAbsolutePath(dirName) + ": Нет такого каталога") + } + } + } + + private def evalLs(args: List[Block], out: OutputStream): Unit = { + var destName = workDirectory + if (args.nonEmpty) { + destName = processBlock(args.head) + destName = getAbsolutePath(destName) + } + val destination = new File(destName) + if (destination.exists()) { + if (destination.isDirectory) { + val notHiddenFiles = destination.listFiles().filterNot(_.isHidden) + notHiddenFiles.foreach { file => + out.write((file.getName + Properties.lineSeparator).getBytes) + } + } else if (destination.isFile) { + out.write((destination.getName + Properties.lineSeparator).getBytes()) + } + } else { + throw new IOException(destName + ": Нет такого файла или каталога") + } + } + private def evalExit(): Unit = { System.exit(0) } private def evalExternal(commandName: String, args: List[Block], in: InputStream, out: OutputStream): Unit = { - val concatArgs = args.map { processBlock } mkString " " + val concatArgs = args.map { + processBlock + } mkString " " val logger = ProcessLogger(line => out.write((line + Properties.lineSeparator).getBytes), _ => ()) if (in.equals(System.in)) { s"$commandName $concatArgs" ! logger @@ -185,4 +229,13 @@ class CommandLineInterpreter { case Literal(value) => value case Substitution(Literal(value)) => environment(value) }.mkString + + private def getAbsolutePath(fileName: String): String = { + val decodedFileName = URLDecoder.decode(fileName, "UTF-8") + if (decodedFileName.startsWith(workDirectory)) { + decodedFileName + } else { + workDirectory + "/" + decodedFileName + } + } } diff --git a/src/test/scala/ru/spbau/mit/softwaredesign/cli/interpreter/InterpreterSpec.scala b/src/test/scala/ru/spbau/mit/softwaredesign/cli/interpreter/InterpreterSpec.scala index 99ca698..57d75a2 100644 --- a/src/test/scala/ru/spbau/mit/softwaredesign/cli/interpreter/InterpreterSpec.scala +++ b/src/test/scala/ru/spbau/mit/softwaredesign/cli/interpreter/InterpreterSpec.scala @@ -1,6 +1,7 @@ package ru.spbau.mit.softwaredesign.cli.interpreter import java.io.{InputStream, OutputStream} +import java.net.URLDecoder import java.nio.channels.{Channels, Pipe} import org.scalatest.FlatSpec @@ -26,16 +27,32 @@ class InterpreterSpec extends FlatSpec { } } - def evalCommandLine(commandLine: String, cli: CommandLineInterpreter, sink: OutputStream): Unit = { - def parseCommandLine(commandLine: String): Composition = { - CommandLineParsers.parse(commandLine) match { - case CommandLineParsers.Success(pipeline, _) => pipeline - case _ => fail - } + def parseCommandLine(commandLine: String): Composition = { + CommandLineParsers.parse(commandLine) match { + case CommandLineParsers.Success(pipeline, _) => pipeline + case _ => fail } + } + + def evalCommandLine(commandLine: String, cli: CommandLineInterpreter, sink: OutputStream): Unit = { + val parsedCommandLine = parseCommandLine(commandLine) + cli.eval(parsedCommandLine, System.in, sink) + sink.close() + } + def evalCdCommand(commandLine: String, cli: CommandLineInterpreter, sink: OutputStream) : Unit ={ + val userDir = System.getProperty("user.dir") + cli.eval(parseCommandLine("cd " + userDir), System.in, sink) val parsedCommandLine = parseCommandLine(commandLine) cli.eval(parsedCommandLine, System.in, sink) + cli.eval(parseCommandLine("pwd"), System.in, sink) + sink.close() + } + + def evalLsCommand(commandLine : String, cli: CommandLineInterpreter, sink:OutputStream) : Unit ={ + val testDirPath = URLDecoder.decode(getClass.getResource("/test_folder").getPath, "UTF-8") + cli.eval(parseCommandLine("cd " + testDirPath), System.in, sink) + cli.eval(parseCommandLine(commandLine), System.in, sink) sink.close() } @@ -64,7 +81,7 @@ class InterpreterSpec extends FlatSpec { val testFilePath = getClass.getResource("/lorem_ipsum.txt").getPath val commandLine = s"cat $testFilePath" evalCommandLine(commandLine, cli, sink) - val expected = Source.fromFile(testFilePath).mkString + val expected = Source.fromFile(URLDecoder.decode(testFilePath, "UTF-8")).mkString val actual = Source.fromInputStream(source).mkString assertResult(expected)(actual) } @@ -184,9 +201,52 @@ class InterpreterSpec extends FlatSpec { } } + it should "interpret cd with no args" in withInterpreter { cli => + withPipe { (sink, source) => + evalCdCommand("cd", cli, sink) + val expected = System.getProperty("user.home") + Properties.lineSeparator + val actual = Source.fromInputStream(source).mkString + assertResult(expected)(actual) + } + } + + it should "interpret cd with args" in withInterpreter { cli => + withPipe { (sink, source) => + val testDirPath = URLDecoder.decode(getClass.getResource("/test_folder").getPath, "UTF-8") + evalCdCommand("cd " + testDirPath, cli, sink) + val expected = testDirPath + Properties.lineSeparator + val actual = Source.fromInputStream(source).mkString + assertResult(expected)(actual) + } + } + + it should "interpret ls with no args" in withInterpreter { cli => + withPipe { (sink, source) => + evalLsCommand("ls", cli, sink) + val testFilePath = URLDecoder.decode(getClass + .getResource("/test_folder/ls_command_result.txt") + .getPath, "UTF-8") + val expected = Source.fromFile(testFilePath).mkString + val actual = Source.fromInputStream(source).mkString + Properties.lineSeparator + assertResult(expected)(actual) + } + } + + it should "interpret ls with args" in withInterpreter { cli => + withPipe { (sink, source) => + evalLsCommand("ls test_folder_2", cli, sink) + val testFilePath = URLDecoder.decode(getClass + .getResource("/test_folder/test_folder_2/ls_command_result.txt") + .getPath, "UTF-8") + val expected = Source.fromFile(testFilePath).mkString + val actual = Source.fromInputStream(source).mkString + Properties.lineSeparator + assertResult(expected)(actual) + } + } + it should "interpret external command with args" in withInterpreter { cli => withPipe { (sink, source) => - val testFilePath = getClass.getResource("/lorem_ipsum.txt").getPath + val testFilePath = URLDecoder.decode(getClass.getResource("/lorem_ipsum.txt").getPath, "UTF-8") val commandLine = s"head -n 1 $testFilePath" evalCommandLine(commandLine, cli, sink) val expected = Source.fromFile(testFilePath).getLines().next() + Properties.lineSeparator