diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..496b3f0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: kotlin + +jdk: + - oraclejdk9 + +os: + - linux + +before_script: + - wget https://services.gradle.org/distributions/gradle-4.5.1-bin.zip + - unzip gradle-4.5.1-bin.zip + - export GRADLE_HOME=$PWD/gradle-4.5.1 + - export PATH=$GRADLE_HOME/bin:$PATH + +script: + - chmod +x build.sh + - bash build.sh \ No newline at end of file diff --git a/bash/README.md b/bash/README.md new file mode 100644 index 0000000..ba1db53 --- /dev/null +++ b/bash/README.md @@ -0,0 +1,10 @@ +# Архитектура +![Image](https://pp.userapi.com/c845321/v845321454/1962dd/W9ATvrz_K6k.jpg) + +На картинке показана упрощённая модель потока данных в архитектуре + +Сначала строка из stdin попадает в лексер, где разбивается на токены + +Оттуда токены передаются в парсер вместе с окружением, где по пайпам токены делятся на команды: первый токен - имя, остальные - аргументы + +Дальше этот массив передаётся контроллеру, который исполняет команды, передавая выходной поток одной команды на вход другой diff --git a/bash/build.gradle b/bash/build.gradle new file mode 100644 index 0000000..60bedc5 --- /dev/null +++ b/bash/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.2.51' +} + +version '1.0-SNAPSHOT' + +apply plugin: 'kotlin' +apply plugin: 'application' + +mainClassName = 'hse.nedikov.bash.Main' + +repositories { + mavenCentral() +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + // https://mvnrepository.com/artifact/junit/junit + testCompile group: 'junit', name: 'junit', version: '4.12' + +} + +jar { + manifest { attributes 'Main-Class': "${mainClassName}" } + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} \ No newline at end of file diff --git a/bash/gradle/wrapper/gradle-wrapper.jar b/bash/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1948b90 Binary files /dev/null and b/bash/gradle/wrapper/gradle-wrapper.jar differ diff --git a/bash/gradle/wrapper/gradle-wrapper.properties b/bash/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d2c45a4 --- /dev/null +++ b/bash/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/bash/gradlew b/bash/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/bash/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/bash/gradlew.bat b/bash/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/bash/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/bash/settings.gradle b/bash/settings.gradle new file mode 100644 index 0000000..2c164f0 --- /dev/null +++ b/bash/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'bash' + diff --git a/bash/src/main/kotlin/hse/nedikov/bash/Controller.kt b/bash/src/main/kotlin/hse/nedikov/bash/Controller.kt new file mode 100644 index 0000000..b26531a --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/Controller.kt @@ -0,0 +1,41 @@ +package hse.nedikov.bash + +import hse.nedikov.bash.logic.Command +import java.io.PipedReader +import java.io.PipedWriter + + +class Controller(val exceptionHandler: (Exception) -> Unit) { + private val env = Environment() + + fun isWorking() = env.isWorking() + + fun runCommandLine(line: String, outputHandler: (String) -> Unit) { + val tokens = Lexer(line, env.variables).lex() + val commands = parse(tokens, env) + val flow = makeFlow(commands); + flow.invoke().forEachLine { outputHandler(it) } + } + + fun makeFlow(commands: ArrayList): () -> PipedReader = + if (commands.isEmpty()) { + { emptyCommandFlow() } + } else commandFlow(commands[0], commands.apply { removeAt(0) }) + + private fun commandFlow(command: Command, commands: ArrayList): () -> PipedReader = { + commands.reversed().fold(command.execute()) { reader, command -> + try { + command.execute(reader) + } catch (e: Exception) { + exceptionHandler(e) + emptyCommandFlow() + } + } + } + + private fun emptyCommandFlow(): PipedReader { + val reader = PipedReader() + PipedWriter(reader).close() + return reader + } +} diff --git a/bash/src/main/kotlin/hse/nedikov/bash/Environment.kt b/bash/src/main/kotlin/hse/nedikov/bash/Environment.kt new file mode 100644 index 0000000..7176d9c --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/Environment.kt @@ -0,0 +1,41 @@ +package hse.nedikov.bash + +import hse.nedikov.bash.Environment.State.* + +/** + * Environment of the interpreter + */ +class Environment { + private enum class State { + Working, Stop + } + + private val varMap = HashMap() + private var state: State = Working + + /** + * Map of the local variables + */ + val variables: (String) -> String = { name -> + varMap[name] ?: "" + } + + /** + * Put the variable with value to the variables map + */ + fun putVariable(name: String, value: String) { + varMap[name] = value + } + + /** + * Changes state to the stop value + */ + fun stopInterpreter() { + state = Stop + } + + /** + * Returns true iff interpreter is still working + */ + fun isWorking(): Boolean = state == Working +} \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/Lexer.kt b/bash/src/main/kotlin/hse/nedikov/bash/Lexer.kt new file mode 100644 index 0000000..7a19019 --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/Lexer.kt @@ -0,0 +1,161 @@ +package hse.nedikov.bash + +import hse.nedikov.bash.Lexer.ParserState.* +import hse.nedikov.bash.exceptions.ParseException +import java.lang.RuntimeException + +/** + * Lexer for bash interpreter + */ +class Lexer(private val data: String, private val variables: (String) -> String) { + enum class ParserState { + InDQuotes, InQuotes, Text, TextVariable, InDQuotesVariable, Identifier, Finished + } + + private var state = Identifier + private var stringBuilder = StringBuilder() + private var varStringBuilder = StringBuilder() + private val result = ArrayList() + + /** + * Parse input to the tokens list + */ + fun lex(): ArrayList { + if (state == Finished) return result + + for (x in data.withIndex()) { + val c = x.value + parseChar(c) + } + if (state == InDQuotes || state == InQuotes) { + throw ParseException("Quotes haven't closed") + } + if (varStringBuilder.isNotEmpty()) { + varStringBuilder = nextVariableBuilder(varStringBuilder, stringBuilder) + } + if (stringBuilder.isNotEmpty()) { + result.add(stringBuilder.toString()) + } + state = Finished + return result + } + + private fun parseChar(c: Char) { + when (state) { + Identifier -> parseIdentifierChar(c) + Text -> parseTextChar(c) + InQuotes -> parseInQuotesChar(c) + InDQuotes -> parseInDQuotesChar(c) + TextVariable -> parseVariableChar(c, Text) + InDQuotesVariable -> parseVariableChar(c, InDQuotes) + Finished -> throw RuntimeException("unexpected finished state") + } + } + + private fun parseIdentifierChar(c: Char) { + when (c) { + matchIdentifier(c) -> stringBuilder.append(c) + matchDigit(c) -> { + if (stringBuilder.isNotEmpty()) stringBuilder.append(c) + else { + stringBuilder.append(c) + state = Text + } + } + '=' -> { + stringBuilder.append(c) + stringBuilder = nextBuilder(stringBuilder, result) + state = Text + } + in whiteSpaces -> { + if (stringBuilder.isNotEmpty()) { + state = Text + parseChar(c) + } + } + else -> { + state = Text + parseChar(c) + } + } + } + + private fun parseVariableChar(c: Char, parent: ParserState) { + when (c) { + matchIdentifier(c) -> varStringBuilder.append(c) + matchDigit(c) -> { + if (varStringBuilder.isNotEmpty()) varStringBuilder.append(c) + else { + varStringBuilder = nextVariableBuilder(varStringBuilder, stringBuilder) + stringBuilder.append(c) + state = Text + } + } + else -> { + varStringBuilder = nextVariableBuilder(varStringBuilder, stringBuilder) + state = parent + parseChar(c) + } + } + } + + private fun nextVariableBuilder(sb: StringBuilder, container: StringBuilder): StringBuilder { + container.append(variables(sb.toString())) + return StringBuilder() + } + + private fun parseInDQuotesChar(c: Char) { + when (c) { + '"' -> { + stringBuilder = nextBuilder(stringBuilder, result) + state = Text + } + '$' -> state = InDQuotesVariable + else -> stringBuilder.append(c) + } + } + + private fun parseInQuotesChar(c: Char) { + when (c) { + '\'' -> { + stringBuilder = nextBuilder(stringBuilder, result) + state = Text + } + else -> stringBuilder.append(c) + } + } + + private fun parseTextChar(c: Char) { + when (c) { + in whiteSpaces -> { + if (stringBuilder.isNotEmpty()) { + stringBuilder = nextBuilder(stringBuilder, result) + } + } + '"' -> state = InDQuotes + '\'' -> state = InQuotes + '$' -> state = TextVariable + else -> stringBuilder.append(c) + } + } + + companion object { + val whiteSpaces = Array(4) { + when (it) { + 0 -> ' ' + 1 -> '\t' + 2 -> '\n' + else -> '\r' + } + } + + fun matchIdentifier(c: Char): Char = if (c in 'a'..'z' || c in 'z'..'Z' || c == '_' || c == '-') c else '_' + + fun matchDigit(c: Char): Char = if (c in '0'..'9') c else '0' + + fun nextBuilder(sb: StringBuilder, container: ArrayList): StringBuilder { + container.add(sb.toString()) + return StringBuilder() + } + } +} \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/Main.kt b/bash/src/main/kotlin/hse/nedikov/bash/Main.kt new file mode 100644 index 0000000..aaa0b76 --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/Main.kt @@ -0,0 +1,17 @@ +package hse.nedikov.bash + +object Main { + @JvmStatic + fun main(args: Array) { + println("welcome!") + val errorHandler: (Exception) -> Unit = { println("error: ${it.message}") } + val controller = Controller(errorHandler) + while (controller.isWorking()) { + try { + controller.runCommandLine(readLine() ?: "") { println(it) } + } catch (e: java.lang.Exception) { + errorHandler(e) + } + } + } +} \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/Parser.kt b/bash/src/main/kotlin/hse/nedikov/bash/Parser.kt new file mode 100644 index 0000000..d734c2f --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/Parser.kt @@ -0,0 +1,33 @@ +package hse.nedikov.bash + +import hse.nedikov.bash.logic.Command + +private const val PIPE = "|" + +/** + * Creates a list of commands by string tokens in pipes + */ +fun parse(tokens: ArrayList, env: Environment): ArrayList { + if (tokens.isNotEmpty() && tokens.last() == PIPE) { + throw Exception("$PIPE in end of command line") + } + val commands = ArrayList() + var i = 0; + while (i < tokens.size) { + val res = parseCommand(tokens, i, env) + i = res.first + commands.add(res.second) + } + return commands +} + +private fun parseCommand(tokens: ArrayList, start: Int, env: Environment): Pair { + var i = start + val name = tokens[i++] + val args = ArrayList() + while (i < tokens.size && tokens[i] != PIPE) { + args.add(tokens[i++]) + } + return (i + 1) to Command.create(name, args, env) +} + diff --git a/bash/src/main/kotlin/hse/nedikov/bash/exceptions/ParseException.kt b/bash/src/main/kotlin/hse/nedikov/bash/exceptions/ParseException.kt new file mode 100644 index 0000000..2d29147 --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/exceptions/ParseException.kt @@ -0,0 +1,8 @@ +package hse.nedikov.bash.exceptions + +import java.lang.Exception + +/** + * Exception for parser + */ +internal class ParseException(message: String) : Exception("Parse Exception: $message") \ 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 new file mode 100644 index 0000000..25eef02 --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/Command.kt @@ -0,0 +1,58 @@ +package hse.nedikov.bash.logic + +import hse.nedikov.bash.Environment +import hse.nedikov.bash.logic.commands.* +import hse.nedikov.bash.logic.environment.Assign +import hse.nedikov.bash.logic.environment.Exit +import java.io.* + +/** + * Base class for all interpreter commands + */ +abstract class Command { + protected abstract fun execute(input: PipedReader, output: PipedWriter) + protected abstract fun execute(output: PipedWriter) + + /** + * Execute the command uses the input. As result returns new reader, which is contains the output of the command + */ + fun execute(input: PipedReader): PipedReader { + val reader = PipedReader() + val writer = PipedWriter(reader) + execute(input, writer) + writer.close() + return reader + } + + /** + * Execute the command and returns new reader, which is contains the output of the command + */ + fun execute(): PipedReader { + val reader = PipedReader() + val writer = PipedWriter(reader) + execute(writer) + writer.close() + return reader + } + + companion object { + private val regex = Regex("[A-z_\\-][A-z_\\-0-9]*=") + + /** + * Creates a command by name, environment and list of arguments + */ + fun create(name: String, args: ArrayList, env: Environment): Command { + if (regex.matches(name)) { + return Assign(name.take(name.length - 1), args, env) + } + return when (name) { + "echo" -> Echo(args) + "wc" -> WordCount(args) + "pwd" -> Pwd() + "exit" -> Exit(env) + "cat" -> Cat(args) + else -> OuterCommand(name, args) + } + } + } +} \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/EnvironmentCommand.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/EnvironmentCommand.kt new file mode 100644 index 0000000..69e7c33 --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/EnvironmentCommand.kt @@ -0,0 +1,8 @@ +package hse.nedikov.bash.logic + +import hse.nedikov.bash.Environment + +/** + * Base class for commands which uses environment + */ +abstract class EnvironmentCommand(open val env: Environment) : Command() \ No newline at end of file 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 new file mode 100644 index 0000000..75c89d6 --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Cat.kt @@ -0,0 +1,33 @@ +package hse.nedikov.bash.logic.commands + +import hse.nedikov.bash.logic.Command +import java.io.* +import java.lang.Exception + +/** + * cat command which prints files entries to the output stream + */ +class Cat(private val arguments: ArrayList) : Command() { + /** + * Prints input to the output if has no arguments and prints entries of files from arguments otherwise + */ + override fun execute(input: PipedReader, output: PipedWriter) { + if (arguments.isNotEmpty()) return execute(output) + input.forEachLine { output.write(it + System.lineSeparator()) } + } + + /** + * Prints entries of files from arguments otherwise + */ + override fun execute(output: PipedWriter) { + for (arg in arguments) { + try { + FileReader(arg).forEachLine { output.write(it + System.lineSeparator()) } + } catch (e: Exception) { + throw Exception("cat: ${e.message}") + } + output.write(System.lineSeparator()) + } + } + +} \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Echo.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Echo.kt new file mode 100644 index 0000000..b32f661 --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Echo.kt @@ -0,0 +1,26 @@ +package hse.nedikov.bash.logic.commands + +import hse.nedikov.bash.logic.Command +import java.io.* +import java.util.* + + +/** + * echo command which prints arguments to the output + */ +class Echo(private val arguments: ArrayList) : Command() { + /** + * Prints arguments which are joined with spaces to the output + */ + override fun execute(output: PipedWriter) { + val result = java.lang.String.join(" ", arguments) + output.write(result.toString()) + } + + /** + * Prints arguments which are joined with spaces to the output + */ + override fun execute(input: PipedReader, output: PipedWriter) { + return execute(output) + } +} \ 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 new file mode 100644 index 0000000..d39457d --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/OuterCommand.kt @@ -0,0 +1,62 @@ +package hse.nedikov.bash.logic.commands + +import hse.nedikov.bash.logic.Command +import java.io.* +import java.util.* +import java.util.concurrent.Executors + +/** + * Class for calling commands in outer interpreter + * @param name name of the command + */ +class OuterCommand(private val name: String, private val arguments: ArrayList) : Command() { + /** + * 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 + */ + override fun execute(input: PipedReader, output: PipedWriter) { + val process = createProcess() + + val processWriter = OutputStreamWriter(process.outputStream) + input.readLines().forEach { processWriter.write(it + System.lineSeparator()) } + processWriter.flush() + processWriter.close() + + startProcess(process, output) + } + + /** + * Calls 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 + */ + private fun startProcess(process: Process, output: PipedWriter) { + val streamGobbler = StreamGobbler(process.inputStream) { s -> output.write(s + System.lineSeparator()) } + val errorGobbler = StreamGobbler(process.errorStream) { s -> output.write(s + System.lineSeparator()) } + val executor = Executors.newSingleThreadExecutor() + executor.submit(streamGobbler) + executor.submit(errorGobbler) + process.waitFor() + executor.shutdown() + } + + override fun execute(output: PipedWriter) { + val process = createProcess() + startProcess(process, output) + } + + private fun createProcess(): Process { + val environmentStart = if (isWindows) "cmd.exe /c" else "sh -c" + val command = StringJoiner(" ", "$name ", "").also { joiner -> 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) + } + } + + companion object { + val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows") + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..990ccc3 --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/Pwd.kt @@ -0,0 +1,25 @@ +package hse.nedikov.bash.logic.commands + +import hse.nedikov.bash.logic.Command +import java.io.* + +/** + * pwd command which prints current working directory + */ +class Pwd : Command() { + /** + * Prints current working directory to the output + */ + override fun execute(input: PipedReader, output: PipedWriter) { + return execute(output) + } + + + /** + * Prints current working directory to the output + */ + override fun execute(output: PipedWriter) { + output.write(System.getProperty("user.dir") + 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 new file mode 100644 index 0000000..71a4de7 --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/commands/WordCount.kt @@ -0,0 +1,61 @@ +package hse.nedikov.bash.logic.commands + +import hse.nedikov.bash.logic.Command +import java.io.* +import java.lang.Exception +import java.util.* + +/** + * wc command which calculates count of lines, words and bytes in files or input + */ +class WordCount(private val arguments: ArrayList) : Command() { + /** + * Calculates count of lines, words and bytes in input if arguments is empty and in files otherwise + */ + override fun execute(input: PipedReader, output: PipedWriter) { + if (arguments.isNotEmpty()) { + return execute(output) + } + val r = calcInput(input) + output.write("${r.lines} ${r.words} ${r.bytes}" + System.lineSeparator()) + output.close() + } + + /** + * Calculates count of lines, words and bytes in files and prints results for each and in total + */ + override fun execute(output: PipedWriter) { + val result = WCResult() + for (arg in arguments) { + try { + val r = calcInput(FileReader(arg)) + output.write("${r.lines} ${r.words} ${r.bytes} $arg" + System.lineSeparator()) + result += r + } catch (e: Exception) { + throw Exception("wc: ${e.message}" + System.lineSeparator()) + } + } + output.write("${result.lines} ${result.words} ${result.bytes} total" + System.lineSeparator()) + } + + private fun calcInput(input: Reader): WCResult { + val result = WCResult() + val text = input.readText() + result.lines = text.split(Regex("\r\n|\r|\n")).size + result.words = StringTokenizer(text).countTokens() + result.bytes = text.toByteArray().size + return result + } + + private class WCResult { + var lines = 0 + var words = 0 + var bytes = 0 + + operator fun plusAssign(other: WCResult) { + lines += other.lines + words += other.words + bytes += other.bytes + } + } +} \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/environment/Assign.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/environment/Assign.kt new file mode 100644 index 0000000..ae66e21 --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/environment/Assign.kt @@ -0,0 +1,30 @@ +package hse.nedikov.bash.logic.environment + +import hse.nedikov.bash.Environment +import hse.nedikov.bash.logic.EnvironmentCommand +import java.io.PipedReader +import java.io.PipedWriter +import java.lang.Exception + +/** + * Class for assigning of variables + * @param name name of variable + */ +class Assign(private val name:String, private val arguments: ArrayList, override val env: Environment) + : EnvironmentCommand(env) { + + /** + * Do nothing in this case + */ + override fun execute(input: PipedReader, output: PipedWriter) { + } + + /** + * Assigns the first argument to the variable in the environment + * @throws Exception if arguments list is empty + */ + override fun execute(output: PipedWriter) { + if (arguments.isEmpty()) throw Exception("Assign exception: expected value after '='") + env.putVariable(name, arguments[0]) + } +} \ No newline at end of file diff --git a/bash/src/main/kotlin/hse/nedikov/bash/logic/environment/Exit.kt b/bash/src/main/kotlin/hse/nedikov/bash/logic/environment/Exit.kt new file mode 100644 index 0000000..1c00c5a --- /dev/null +++ b/bash/src/main/kotlin/hse/nedikov/bash/logic/environment/Exit.kt @@ -0,0 +1,24 @@ +package hse.nedikov.bash.logic.environment + +import hse.nedikov.bash.Environment +import hse.nedikov.bash.logic.EnvironmentCommand +import java.io.* + +/** + * Class for command which closes the interpreter + */ +class Exit(override val env: Environment) : EnvironmentCommand(env) { + /** + * Stops the interpreter + */ + override fun execute(input: PipedReader, output: PipedWriter) { + return execute(output) + } + + /** + * Stops the interpreter + */ + override fun execute(output: PipedWriter) { + env.stopInterpreter() + } +} \ No newline at end of file diff --git a/bash/src/test/kotlin/hse/nedikov/bash/LexerTest.kt b/bash/src/test/kotlin/hse/nedikov/bash/LexerTest.kt new file mode 100644 index 0000000..24bae29 --- /dev/null +++ b/bash/src/test/kotlin/hse/nedikov/bash/LexerTest.kt @@ -0,0 +1,94 @@ +package hse.nedikov.bash + +import org.junit.Assert.assertEquals +import org.junit.Test + +class LexerTest { + @Test + fun lexEmptyString() { + val r = Lexer("") { _ -> "" }.lex() + assertEquals(0, r.size) + } + + @Test + fun lexWord() { + val r = Lexer("lol") { _ -> "" }.lex() + assertEquals(1, r.size) + assertEquals("lol", r[0]) + } + + @Test + fun lexTwoWords() { + val r = Lexer("lol lal") { "" }.lex() + assertEquals(2, r.size) + assertEquals("lol", r[0]) + assertEquals("lal", r[1]) + } + + @Test + fun lexVariable() { + val r = Lexer("\$lol", lolToLal).lex() + assertEquals(1, r.size) + assertEquals("lal", r[0]) + } + + @Test + fun lexVariable2() { + val r = Lexer("\$lol \$lel", lolToLal).lex() + assertEquals(1, r.size) + assertEquals("lal", r[0]) + } + + @Test + fun lexInQuotes() { + val r = Lexer("'lal lol '") { "" }.lex() + assertEquals(1, r.size) + assertEquals("lal lol ", r[0]) + } + + @Test + fun lexInDQuotes() { + val r = Lexer("\"lal lol \"") { "" }.lex() + assertEquals(1, r.size) + assertEquals("lal lol ", r[0]) + } + + @Test + fun lexInDQuotesWithVariable() { + val r = Lexer("\"lal \$lol \"", lolToLal).lex() + assertEquals(1, r.size) + assertEquals("lal lal ", r[0]) + } + + @Test + fun lexInQuotesWithVariable() { + val r = Lexer("'lal \$lol '", lolToLal).lex() + assertEquals(1, r.size) + assertEquals("lal \$lol ", r[0]) + } + + @Test + fun lexAssign() { + val r = Lexer(" lol=kek", lolToLal).lex() + assertEquals(2, r.size) + assertEquals("lol=", r[0]) + assertEquals("kek", r[1]) + } + + @Test + fun lexAssignInQuotes() { + val r = Lexer(" lol='kek lol'", lolToLal).lex() + assertEquals(2, r.size) + assertEquals("lol=", r[0]) + assertEquals("kek lol", r[1]) + } + + companion object { + val lolToLal: (String) -> String = { + when (it) { + "lol" -> "lal" + else -> "" + } + } + } +} \ No newline at end of file diff --git a/bash/src/test/kotlin/hse/nedikov/bash/ParserTest.kt b/bash/src/test/kotlin/hse/nedikov/bash/ParserTest.kt new file mode 100644 index 0000000..1787054 --- /dev/null +++ b/bash/src/test/kotlin/hse/nedikov/bash/ParserTest.kt @@ -0,0 +1,49 @@ +package hse.nedikov.bash + +import hse.nedikov.bash.logic.commands.Echo +import hse.nedikov.bash.logic.commands.WordCount +import org.junit.Assert.* +import org.junit.Test +import java.util.* + +class ParserTest { + @Test + fun empty() { + assertTrue(parse(ArrayList(), environment).isEmpty()) + } + + @Test + fun simpleCommand() { + val result = parse(list("echo"), environment) + assertEquals(1, result.size) + assertTrue(result[0] is Echo) + } + + @Test + fun commandWithArgs() { + val result = parse(list("echo", "lol", "kek"), environment) + assertEquals(1, result.size) + assertTrue(result[0] is Echo) + } + + @Test + fun pipedCommands() { + val result = parse(list("echo", "|", "wc"), environment) + assertEquals(2, result.size) + assertTrue(result[0] is Echo) + assertTrue(result[1] is WordCount) + } + + + @Test + fun pipedCommandsWithArg() { + val result = parse(list("echo", "lol", "kek", "|", "wc", "cv"), environment) + assertEquals(2, result.size) + assertTrue(result[0] is Echo) + assertTrue(result[1] is WordCount) + } + + companion object { + val environment = Environment() + } +} \ No newline at end of file diff --git a/bash/src/test/kotlin/hse/nedikov/bash/TestUtil.kt b/bash/src/test/kotlin/hse/nedikov/bash/TestUtil.kt new file mode 100644 index 0000000..8038f4a --- /dev/null +++ b/bash/src/test/kotlin/hse/nedikov/bash/TestUtil.kt @@ -0,0 +1,5 @@ +package hse.nedikov.bash + +import java.util.ArrayList + +fun list(vararg values: String): ArrayList = ArrayList().apply { addAll(values) } \ No newline at end of file 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 new file mode 100644 index 0000000..5547648 --- /dev/null +++ b/bash/src/test/kotlin/hse/nedikov/bash/logic/commands/CommandsTest.kt @@ -0,0 +1,73 @@ +package hse.nedikov.bash.logic.commands + +import org.junit.Test +import java.io.PipedReader +import java.io.PipedWriter +import java.util.* +import hse.nedikov.bash.list +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue + +class CommandsTest { + @Test + fun echoSimple() { + val reader = Echo(list("lol")).execute() + assertEquals("lol", stringFromReader(reader)) + } + + @Test + fun echoMultiArguments() { + val reader = Echo(list("lol", "lal", "lel")).execute() + assertEquals("lol lal lel", stringFromReader(reader)) + } + + @Test + fun echoInputStream() { + val reader = Echo(list("lol", "lal", "lel")).execute(readerFromString("kekes")) + assertEquals("lol lal lel", stringFromReader(reader)) + } + + @Test + fun catSimple() { + val reader = Echo(list()).execute() + assertEquals("", stringFromReader(reader)) + } + + @Test + fun catInputStream() { + val reader = Cat(list()).execute(readerFromString("kekes leles")) + assertEquals("kekes leles", stringFromReader(reader)) + } + + @Test + fun pwdSimple() { + val reader = Pwd().execute() + assertTrue(stringFromReader(reader).isNotEmpty()) + } + + @Test + fun pwdSimpleWithInputStream() { + val reader = Pwd().execute(readerFromString("kekes leles")) + assertTrue(stringFromReader(reader).isNotEmpty()) + } + + @Test + fun wordCountSimple() { + val reader = WordCount(list()).execute(readerFromString(" lol kek cheburek")) + assertEquals("1 3 19", stringFromReader(reader)) + } + + companion object { + fun readerFromString(string: String): PipedReader { + val reader = PipedReader() + PipedWriter(reader).apply { write(string) }.close() + return reader + } + + fun stringFromReader(reader: PipedReader): String { + val joiner = StringJoiner(System.lineSeparator()) + reader.readLines().forEach { joiner.add(it) } + return joiner.toString() + } + } +} \ No newline at end of file diff --git a/bash/src/test/kotlin/hse/nedikov/bash/logic/environment/EnvironmentCommandsTest.kt b/bash/src/test/kotlin/hse/nedikov/bash/logic/environment/EnvironmentCommandsTest.kt new file mode 100644 index 0000000..6f7f1e0 --- /dev/null +++ b/bash/src/test/kotlin/hse/nedikov/bash/logic/environment/EnvironmentCommandsTest.kt @@ -0,0 +1,30 @@ +package hse.nedikov.bash.logic.environment + +import hse.nedikov.bash.Environment +import hse.nedikov.bash.list +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class EnvironmentCommandsTest { + lateinit var environment: Environment + @Before + fun createEnvironment() { + environment = Environment() + environment.putVariable("x", "lel") + } + + @Test + fun testAssign() { + assertEquals("lel", environment.variables("x")) + Assign("x", list("lal"), environment).execute() + assertEquals("lal", environment.variables("x")) + } + + @Test + fun testExit() { + assertTrue(environment.isWorking()) + Exit(environment).execute() + assertFalse(environment.isWorking()) + } +} \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..a983b02 --- /dev/null +++ b/build.sh @@ -0,0 +1,9 @@ +#! /bin/bash + +for hw in $(find . -maxdepth 1 -mindepth 1 -type d) ; do + if [ -f "$hw/build.gradle" ] ; then + echo "processing $hw" + cd "$hw" && gradle check && cd .. || { echo "check failed" ; exit 1 ; } + echo "finishing $hw" + fi +done \ No newline at end of file