diff --git a/bash/src/main/kotlin/hse/nedikov/bash/Environment.kt b/bash/src/main/kotlin/hse/nedikov/bash/Environment.kt index 7176d9c..89cb3b8 100644 --- a/bash/src/main/kotlin/hse/nedikov/bash/Environment.kt +++ b/bash/src/main/kotlin/hse/nedikov/bash/Environment.kt @@ -1,6 +1,7 @@ package hse.nedikov.bash import hse.nedikov.bash.Environment.State.* +import java.io.File /** * Environment of the interpreter @@ -12,6 +13,7 @@ class Environment { private val varMap = HashMap() private var state: State = Working + private var workingDirectory = File("./") /** * Map of the local variables @@ -38,4 +40,20 @@ class Environment { * Returns true iff interpreter is still working */ fun isWorking(): Boolean = state == Working + + fun changeDirectory(newPath: String): Boolean { + val newDirectory = getCanonicalFile(newPath) + if (newDirectory.isDirectory) { + workingDirectory = newDirectory + } + return newDirectory.isDirectory + } + + fun getCanonicalPath(path: String): String { + return getCanonicalFile(path).canonicalPath + } + + fun getCanonicalFile(path: String): File { + return workingDirectory.resolve(path) + } } \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/Command.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/Command.kt index 8954b4f..7af92d7 100644 --- a/bash/src/main/kotlin/hse/nedikov/bash/logic/Command.kt +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/Command.kt @@ -47,12 +47,14 @@ abstract class Command { } return when (name) { "echo" -> Echo(args) - "wc" -> WordCount(args) - "pwd" -> Pwd() + "wc" -> WordCount(args, env) + "pwd" -> Pwd(env) "exit" -> Exit(env) - "cat" -> Cat(args) - "grep" -> Grep(args) - else -> OuterCommand(name, args) + "cat" -> Cat(args, env) + "grep" -> Grep(args, env) + "cd" -> Cd(args, env) + "ls" -> Ls(args, env) + else -> OuterCommand(name, args, env) } } } diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Cat.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Cat.kt index f117f99..8ec1833 100644 --- a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Cat.kt +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Cat.kt @@ -1,13 +1,14 @@ package hse.nedikov.bash.logic.commands -import hse.nedikov.bash.logic.Command +import hse.nedikov.bash.Environment +import hse.nedikov.bash.logic.EnvironmentCommand import java.io.* import java.lang.Exception /** * cat command which prints files entries to the output stream */ -class Cat(private val arguments: ArrayList) : Command() { +class Cat(private val arguments: ArrayList, override val env: Environment) : EnvironmentCommand(env) { /** * Prints input to the output if has no arguments and prints entries of files from arguments otherwise */ @@ -22,7 +23,7 @@ class Cat(private val arguments: ArrayList) : Command() { override fun execute(output: PipedWriter) { for (arg in arguments) { try { - FileReader(arg).forEachLine { output.write(arg) } + FileReader(env.getCanonicalPath(arg)).forEachLine { output.write(it + System.lineSeparator()) } } catch (e: Exception) { output.write("cat: ${e.message}") } diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Cd.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Cd.kt new file mode 100644 index 0000000..8b2606e --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Cd.kt @@ -0,0 +1,31 @@ +package hse.nedikov.bash.logic.commands + +import hse.nedikov.bash.Environment +import hse.nedikov.bash.logic.Command +import java.io.PipedReader +import java.io.PipedWriter +import java.lang.RuntimeException + +/** + * cd command which changes the working directory + */ +class Cd(private val arguments: ArrayList, private val env: Environment) : Command() { + /** + * Changes the working directory + */ + override fun execute(input: PipedReader, output: PipedWriter) { + return execute(output) + } + + /** + * Changes the working directory + */ + override fun execute(output: PipedWriter) { + if (arguments.size > 1) { + throw RuntimeException("cd: too many arguments") + } + if (!env.changeDirectory(arguments.getOrElse(0) { System.getProperty("user.home") })) { + throw RuntimeException("cd: directory not found") + } + } +} \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Grep.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Grep.kt index 6cc6ade..3983751 100644 --- a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Grep.kt +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Grep.kt @@ -3,7 +3,8 @@ package hse.nedikov.bash.logic.commands import com.xenomachina.argparser.ArgParser import com.xenomachina.argparser.ShowHelpException import com.xenomachina.argparser.default -import hse.nedikov.bash.logic.Command +import hse.nedikov.bash.Environment +import hse.nedikov.bash.logic.EnvironmentCommand import java.io.* import java.lang.IllegalArgumentException import java.util.ArrayList @@ -11,7 +12,7 @@ import java.util.ArrayList /** * grep command which prints all lines with pattern matched substring */ -class Grep(private val arguments: ArrayList) : Command() { +class Grep(private val arguments: ArrayList, override val env: Environment) : EnvironmentCommand(env) { /** * Takes lines for grep from input */ @@ -25,7 +26,8 @@ class Grep(private val arguments: ArrayList) : Command() { */ override fun execute(output: PipedWriter) { val args = parseArguments(::GrepArgs, output) ?: return - grep(args.file, output, args) + val input = FileReader(env.getCanonicalPath(args.file)) + grep(input, output, args) } private fun parseArguments(constructor: (ArgParser) -> T, output: Writer): T? { @@ -35,11 +37,11 @@ class Grep(private val arguments: ArrayList) : Command() { } catch (e: ShowHelpException) { println(e.printUserMessage(output, "grep", 80)) } - return null; + return null } private fun grep(input: Reader, output: PipedWriter, args: PipedGrepArgs) { - var linesToWrite = 0; + var linesToWrite = 0 input.forEachLine { val matchString = if (args.ignoreCase) it.toLowerCase() else it val matchPattern = if (args.ignoreCase) args.pattern.toLowerCase() else args.pattern @@ -47,7 +49,7 @@ class Grep(private val arguments: ArrayList) : Command() { val regex = Regex(".*?$wordEnd$matchPattern$wordEnd.*") if (matchString.matches(regex)) { output.write("$it\n") - linesToWrite = args.afterContext; + linesToWrite = args.afterContext } else if (linesToWrite > 0) { output.write("$it\n") linesToWrite-- @@ -67,5 +69,5 @@ private open class PipedGrepArgs(parser: ArgParser) { } private class GrepArgs(parser: ArgParser) : PipedGrepArgs(parser) { - val file by parser.positional("FILE") { FileReader(this) } + val file by parser.positional("FILE") { this } } \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Ls.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Ls.kt new file mode 100644 index 0000000..b728e76 --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Ls.kt @@ -0,0 +1,37 @@ +package hse.nedikov.bash.logic.commands + +import hse.nedikov.bash.Environment +import hse.nedikov.bash.logic.Command +import java.io.File +import java.io.PipedReader +import java.io.PipedWriter + +/** + * ls command which prints all files in current directory + */ +class Ls(private val arguments: ArrayList, private val env: Environment) : Command() { + /** + * Prints current directory + */ + override fun execute(input: PipedReader, output: PipedWriter) { + return execute(output) + } + + /** + * Prints current directory + */ + override fun execute(output: PipedWriter) { + if (arguments.size > 1) { + throw RuntimeException("ls: too many arguments") + } + printFile(env.getCanonicalFile(arguments.getOrElse(0) { "./" }), output) + } + + private fun printFile(file: File, output: PipedWriter) { + when { + file.isFile -> output.write(file.name + System.lineSeparator()) + file.isDirectory -> file.listFiles().sorted().forEach { output.write(it.name + System.lineSeparator()) } + else -> throw RuntimeException("ls: file or directory not found" + System.lineSeparator()) + } + } +} \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/OuterCommand.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/OuterCommand.kt index ec44fd5..3442a16 100644 --- a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/OuterCommand.kt +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/OuterCommand.kt @@ -1,18 +1,17 @@ package hse.nedikov.bash.logic.commands -import hse.nedikov.bash.logic.Command +import hse.nedikov.bash.Environment +import hse.nedikov.bash.logic.EnvironmentCommand import java.io.* -import java.util.concurrent.Executors -import java.io.InputStreamReader -import java.io.BufferedReader import java.util.* -import java.util.concurrent.TimeUnit /** * Class for calling commands in outer interpreter * @param name name of the command */ -class OuterCommand(private val name: String, private val arguments: ArrayList) : Command() { +class OuterCommand(private val name: String, + private val arguments: ArrayList, + override val env: Environment) : EnvironmentCommand(env) { /** * Calls the command in outer interpreter and print theirs output or error to the output * in case when the command is executed in less than 10 seconds @@ -22,7 +21,7 @@ class OuterCommand(private val name: String, private val arguments: ArrayList output.write("$s\n") } - val errorGobbler = StreamGobbler(process.errorStream) { s -> output.write("$s\n") } - val executor = Executors.newSingleThreadExecutor() - executor.submit(streamGobbler) - executor.submit(errorGobbler) - process.waitFor(10, TimeUnit.SECONDS) + process.inputStream.bufferedReader().copyTo(output) + process.errorStream.bufferedReader().copyTo(output) + process.waitFor() } override fun execute(output: PipedWriter) { @@ -46,15 +42,9 @@ class OuterCommand(private val name: String, private val arguments: ArrayList arguments.forEach { joiner.add(it) } }.toString() - return Runtime.getRuntime().exec("$environmentStart $command") - } - - private class StreamGobbler(private val inputStream: InputStream, private val consumer: (String) -> Unit) : Runnable { - override fun run() { - BufferedReader(InputStreamReader(inputStream)).lines().forEach(consumer) - } + return Runtime.getRuntime().exec("$environmentStart $command", null, env.getCanonicalFile("./")) } companion object { diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Pwd.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Pwd.kt index 3084c17..cbd7c29 100644 --- a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Pwd.kt +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Pwd.kt @@ -1,12 +1,13 @@ package hse.nedikov.bash.logic.commands -import hse.nedikov.bash.logic.Command +import hse.nedikov.bash.Environment +import hse.nedikov.bash.logic.EnvironmentCommand import java.io.* /** * pwd command which prints current working directory */ -class Pwd : Command() { +class Pwd(override val env: Environment) : EnvironmentCommand(env) { /** * Prints current working directory to the output */ @@ -19,7 +20,7 @@ class Pwd : Command() { * Prints current working directory to the output */ override fun execute(output: PipedWriter) { - output.write(System.getProperty("user.dir") + "\n") + output.write(env.getCanonicalPath("./") + System.lineSeparator()) } } \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/WordCount.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/WordCount.kt index a51069f..66c0da1 100644 --- a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/WordCount.kt +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/WordCount.kt @@ -1,13 +1,14 @@ package hse.nedikov.bash.logic.commands -import hse.nedikov.bash.logic.Command +import hse.nedikov.bash.Environment +import hse.nedikov.bash.logic.EnvironmentCommand import java.io.* import java.lang.Exception /** * wc command which calculates count of lines, words and bytes in files or input */ -class WordCount(private val arguments: ArrayList) : Command() { +class WordCount(private val arguments: ArrayList, override val env: Environment) : EnvironmentCommand(env) { /** * Calculates count of lines, words and bytes in input if arguments is empty and in files otherwise */ @@ -27,7 +28,7 @@ class WordCount(private val arguments: ArrayList) : Command() { val result = WCResult() for (arg in arguments) { try { - val r = calcInput(FileReader(arg)) + val r = calcInput(FileReader(env.getCanonicalPath(arg))) output.write("${r.lines} ${r.words} ${r.bytes} $arg\n") } catch (e: Exception) { output.write("wc: ${e.message}\n") diff --git a/bash/src/test/kotlin/hse/nedikov/bash/logic/commands/CommandsTest.kt b/bash/src/test/kotlin/hse/nedikov/bash/logic/commands/CommandsTest.kt index 0723522..0963e74 100644 --- a/bash/src/test/kotlin/hse/nedikov/bash/logic/commands/CommandsTest.kt +++ b/bash/src/test/kotlin/hse/nedikov/bash/logic/commands/CommandsTest.kt @@ -1,5 +1,6 @@ package hse.nedikov.bash.logic.commands +import hse.nedikov.bash.Environment import org.junit.Test import java.io.PipedReader import java.io.PipedWriter @@ -7,8 +8,120 @@ import java.util.* import hse.nedikov.bash.list import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import java.io.File class CommandsTest { + + @Rule + @JvmField + val tmpFolder = TemporaryFolder() + + @Test(expected = RuntimeException::class) + fun lsTooManyArgumentsTest() { + Ls(list("arg1", "arg2"), Environment()).execute() + } + + @Test(expected = RuntimeException::class) + fun lsDirectoryNotFoundTest() { + Ls(list("baddirectoryyyyyy"), Environment()).execute() + } + + @Test + fun lsNoArgumentsTest() { + tmpFolder.newFile("file") + tmpFolder.newFile("oneMoreFile") + tmpFolder.newFolder("folder") + tmpFolder.newFolder("oneMoreFolder") + val env = Environment() + env.changeDirectory(tmpFolder.root.canonicalPath) + val reader = Ls(list(), env).execute() + assertEquals(""" + file + folder + oneMoreFile + oneMoreFolder + """.trimIndent(), stringFromReader(reader)) + tmpFolder.delete() + } + + @Test + fun lsOneArgumentTest() { + tmpFolder.newFolder("folder", "oneMoreFolder") + tmpFolder.newFile("folder/file") + tmpFolder.newFile("folder/oneMoreFile") + val env = Environment() + env.changeDirectory(tmpFolder.root.canonicalPath) + val reader = Ls(list("folder"), env).execute() + assertEquals(""" + file + oneMoreFile + oneMoreFolder + """.trimIndent(), stringFromReader(reader)) + tmpFolder.delete() + } + + @Test + fun lsFileTest() { + tmpFolder.newFile("file") + val env = Environment() + env.changeDirectory(tmpFolder.root.canonicalPath) + val reader = Ls(list("file"), env).execute() + assertEquals("file", stringFromReader(reader)) + tmpFolder.delete() + } + + @Test(expected = RuntimeException::class) + fun cdTooManyArgumentsTest() { + Cd(list("arg1", "arg2"), Environment()).execute() + } + + @Test + fun cdNoArgumentsTest() { + val env = Environment() + Cd(list(), env).execute() + assertEquals(System.getProperty("user.home"), env.getCanonicalPath("./")) + } + + + @Test(expected = RuntimeException::class) + fun cdDirectoryNotFoundTest() { + Cd(list("baddirectoryyyyyy"), Environment()).execute() + } + + @Test + fun cdAndThenLsTest() { + tmpFolder.newFolder("folder", "oneMoreFolder") + val env = Environment() + Cd(list(tmpFolder.root.canonicalPath), env).execute() + val reader = Ls(list("folder"), env).execute() + assertEquals("oneMoreFolder", stringFromReader(reader)) + tmpFolder.delete() + } + + @Test + fun cdRelativePathTest() { + val env = Environment() + Cd(list("../"), env).execute() + assertEquals(File("../").canonicalPath, env.getCanonicalPath("./")) + } + + @Test + fun cdAbsoluteTest() { + val env = Environment() + Cd(list(File("../").canonicalPath), env).execute() + assertEquals(File("../").canonicalPath, env.getCanonicalPath("./")) + } + + @Test + fun cdAndThenPwdTest() { + val env = Environment() + Cd(list("../"), env).execute() + val reader = Pwd(env).execute() + assertEquals(File("../").canonicalPath, stringFromReader(reader)) + } + @Test fun echoSimple() { val reader = Echo(list("lol")).execute() @@ -35,31 +148,31 @@ class CommandsTest { @Test fun catInputStream() { - val reader = Cat(list()).execute(readerFromString("kekes leles")) + val reader = Cat(list(), Environment()).execute(readerFromString("kekes leles")) assertEquals("kekes leles", stringFromReader(reader)) } @Test fun pwdSimple() { - val reader = Pwd().execute() + val reader = Pwd(Environment()).execute() assertTrue(stringFromReader(reader).isNotEmpty()) } @Test fun pwdSimpleWithInputStream() { - val reader = Pwd().execute(readerFromString("kekes leles")) + val reader = Pwd(Environment()).execute(readerFromString("kekes leles")) assertTrue(stringFromReader(reader).isNotEmpty()) } @Test fun wordCountSimple() { - val reader = WordCount(list()).execute(readerFromString("lol kek cheburek")) + val reader = WordCount(list(), Environment()).execute(readerFromString("lol kek cheburek")) assertEquals("1 3 16", stringFromReader(reader)) } @Test fun grepTest() { - val reader = Grep(list("lol")).execute(readerFromString(keklolString)) + val reader = Grep(list("lol"), Environment()).execute(readerFromString(keklolString)) assertEquals(""" lol kek kek lol @@ -71,7 +184,7 @@ class CommandsTest { @Test fun grepWordRegexpTest() { - val reader = Grep(list("lol", "-w")).execute(readerFromString(keklolString)) + val reader = Grep(list("lol", "-w"), Environment()).execute(readerFromString(keklolString)) assertEquals(""" lol kek kek lol @@ -81,7 +194,7 @@ class CommandsTest { @Test fun grepIgnoreCaseTest() { - val reader = Grep(list("LoL", "-i")).execute(readerFromString(keklolString)) + val reader = Grep(list("LoL", "-i"), Environment()).execute(readerFromString(keklolString)) assertEquals(""" lol kek kek lol @@ -94,9 +207,9 @@ class CommandsTest { @Test fun grepAfterContextTest() { - var reader = Grep(list("lol", "-A", "5")).execute(readerFromString(keklolString)) + var reader = Grep(list("lol", "-A", "5"), Environment()).execute(readerFromString(keklolString)) assertEquals(keklolString, stringFromReader(reader)) - reader = Grep(list("lol", "-A", "1")).execute(readerFromString(keklolString)) + reader = Grep(list("lol", "-A", "1"), Environment()).execute(readerFromString(keklolString)) assertEquals(""" lol kek kek lol