diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f06267 --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ + +# Created by https://www.gitignore.io/api/gradle,jetbrains+all +# Edit at https://www.gitignore.io/?templates=gradle,jetbrains+all + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin + .idea/sonarlint + +### Gradle ### +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +# End of https://www.gitignore.io/api/gradle,jetbrains+all \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..42e6556 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# CLI +### Сборка проекта: +Для сборки проекта используется [gradle](https://docs.gradle.org/current/userguide/installation.html). + +Сборка проекта происходит с помощью команды: +```$bash +gradle build +``` +Данная команда создаст ZIP и TAR архивы со всем необходимым для запуска в `build/distributions`. + +## Основные компоненты системы: +- IToken - представление токена команды в системе +- Parser - модуль обработки строкового представления команды и перевода его в список IToken +- Context - объект, служащий для хранения текущего состояния системы (текущие переменные, флаг завершения программы) +- Executor - модуль выполнения команды по списку токенов + +## Описание работы +Команда разбивается на токены с помощью компоненты Parser, каждый токен умеет выводить свое строковое представление. Так, например, токен переменная возвращает значение переменной в качестве значения. +Затем, полученный список токенов передается в модуль Executor, который исполняет команду, +на каждом шаге работы запоминая результат работы последней исполненной команды. Результат последнего шага исполнения является результатом исполнения всей команды. + +#### Схема выполнения команды: +![Схема работы](images/schema.png) + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9056608 --- /dev/null +++ b/build.gradle @@ -0,0 +1,17 @@ +group 'ru.ifmo' +version '1.0-SNAPSHOT' + +apply plugin: 'java' +apply plugin: 'application' + +sourceCompatibility = 1.9 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' +} + +mainClassName = 'ru.ifmo.cli_application.CLI' \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..01b8bf6 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0142f8c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Feb 25 07:13:53 MSK 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/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/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/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/images/schema.png b/images/schema.png new file mode 100644 index 0000000..1504fb0 Binary files /dev/null and b/images/schema.png differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..d02d86e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'cli-application' +rootProject.name = 'cli-application' + diff --git a/src/main/java/ru/ifmo/cli_application/Application.java b/src/main/java/ru/ifmo/cli_application/Application.java new file mode 100644 index 0000000..34bd817 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/Application.java @@ -0,0 +1,56 @@ +package ru.ifmo.cli_application; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Scanner; + +import ru.ifmo.cli_application.parser.Parser; +import ru.ifmo.cli_application.parser.SimpleParser; +import ru.ifmo.cli_application.tokens.IToken; + +/** + * Main CLI application class + */ +public class Application { + private Context ctx; + private Parser parser; + private Executor executor; + private InputStream inputStream; + private OutputStream outputStream; + + /** + * CLI application constructor + * @param inputStream stream for commands input + * @param outputStream stream for commands result output + */ + public Application(InputStream inputStream, OutputStream outputStream) { + ctx = new Context(); + parser = new SimpleParser(ctx); + this.inputStream = inputStream; + this.outputStream = outputStream; + + executor = new Executor(ctx); + } + + /** + * Method for starting cli application + * @throws IOException in case of input or output stream is not working + */ + public void start() throws IOException { + Scanner scanner = new Scanner(inputStream); + + while (!ctx.isClosed()) { + try { + List tokens = parser.parseTokens(scanner.nextLine()); + String result = executor.execute(tokens); + outputStream.write(result.getBytes()); + outputStream.flush(); + } catch (Throwable e) { + outputStream.write(e.getMessage().getBytes()); + outputStream.flush(); + } + } + } +} diff --git a/src/main/java/ru/ifmo/cli_application/CLI.java b/src/main/java/ru/ifmo/cli_application/CLI.java new file mode 100644 index 0000000..cfea95d --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/CLI.java @@ -0,0 +1,10 @@ +package ru.ifmo.cli_application; + +import java.io.IOException; + +public class CLI { + public static void main(String[] args) throws IOException { + Application application = new Application(System.in, System.out); + application.start(); + } +} diff --git a/src/main/java/ru/ifmo/cli_application/Context.java b/src/main/java/ru/ifmo/cli_application/Context.java new file mode 100644 index 0000000..ebb9ef5 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/Context.java @@ -0,0 +1,25 @@ +package ru.ifmo.cli_application; + +import java.util.HashMap; +import java.util.Map; + +public class Context { + private Map variables = new HashMap<>(); + private boolean isClosed = false; + + public void setVariable(String variable, String result) { + this.variables.put(variable, result); + } + + public String getVariable(String variable) { + return this.variables.getOrDefault(variable, ""); + } + + public void exit() { + isClosed = true; + } + + public boolean isClosed() { + return isClosed; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/Executor.java b/src/main/java/ru/ifmo/cli_application/Executor.java new file mode 100644 index 0000000..09cdbc7 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/Executor.java @@ -0,0 +1,43 @@ +package ru.ifmo.cli_application; + +import java.util.ArrayList; +import java.util.List; + +import ru.ifmo.cli_application.tokens.IDelimiter; +import ru.ifmo.cli_application.tokens.IExecutable; +import ru.ifmo.cli_application.tokens.IToken; + +public class Executor { + private Context ctx; + + public Executor(Context ctx) { + this.ctx = ctx; + } + + public String execute(List tokens) { + String prevOutput = ""; + if (tokens.isEmpty()) { + return ""; + } + List tokensToExecute = new ArrayList<>(); + IExecutable executableCommand = null; + for (IToken token : tokens) { + if (executableCommand == null) { + executableCommand = ((IExecutable) token); + continue; + } + if (token instanceof IDelimiter) { + prevOutput = executableCommand.execute(tokensToExecute, prevOutput); + executableCommand = null; + tokensToExecute.clear(); + continue; + } + tokensToExecute.add(token); + } + if (executableCommand != null) { + prevOutput = executableCommand.execute(tokensToExecute, prevOutput); + } + prevOutput += "\n"; + return prevOutput; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/commands/CatCommand.java b/src/main/java/ru/ifmo/cli_application/commands/CatCommand.java new file mode 100644 index 0000000..d1186ee --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/commands/CatCommand.java @@ -0,0 +1,36 @@ +package ru.ifmo.cli_application.commands; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Stream; + +import ru.ifmo.cli_application.tokens.IExecutable; +import ru.ifmo.cli_application.tokens.IToken; + +/** + * Class for concatenation files command + */ +public class CatCommand implements IToken, IExecutable { + @Override + public String execute(List args, String inputStream) { + if (args.size() == 0) { + return inputStream; + } + StringBuilder resultString = new StringBuilder(); + for (IToken fileName : args) { + try (Stream lines = Files.lines(Paths.get(fileName.getValue()))) { + lines.forEach((s)->resultString.append(s).append('\n')); + } catch (IOException e) { + return "File not found: " + fileName.getValue(); + } + } + return resultString.toString(); + } + + @Override + public String getValue() { + return "cat"; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/commands/Command.java b/src/main/java/ru/ifmo/cli_application/commands/Command.java new file mode 100644 index 0000000..e606f3f --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/commands/Command.java @@ -0,0 +1,42 @@ +package ru.ifmo.cli_application.commands; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import ru.ifmo.cli_application.tokens.IExecutable; +import ru.ifmo.cli_application.tokens.IToken; + +/** + * Class for execution external bash commands + */ +public class Command implements IToken, IExecutable { + private String commandName; + + public Command(String commandName) { + this.commandName = commandName; + } + + @Override + public String execute(List args, String inputStream) { + try { + List result = new ArrayList<>(); + result.add(commandName); + for (IToken arg : args) { + result.add(arg.getValue()); + } + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.command(result); + Process process = processBuilder.start(); + InputStream processOutput = process.getInputStream(); + return new String(processOutput.readAllBytes()); + } catch (IOException e) { + return commandName + ": command not found"; + } + } + + public String getValue() { + return commandName; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/commands/EchoCommand.java b/src/main/java/ru/ifmo/cli_application/commands/EchoCommand.java new file mode 100644 index 0000000..741f8cc --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/commands/EchoCommand.java @@ -0,0 +1,26 @@ +package ru.ifmo.cli_application.commands; + +import java.util.List; + +import ru.ifmo.cli_application.tokens.IExecutable; +import ru.ifmo.cli_application.tokens.IToken; + +/** + * Class for echo command + */ +public class EchoCommand implements IToken, IExecutable { + @Override + public String execute(List args, String inputStream) { + StringBuilder stringBuilder = new StringBuilder(); + for (IToken arg : args) { + stringBuilder.append(arg.getValue()); + stringBuilder.append(' '); + } + return stringBuilder.toString(); + } + + @Override + public String getValue() { + return "echo"; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/commands/ExitCommand.java b/src/main/java/ru/ifmo/cli_application/commands/ExitCommand.java new file mode 100644 index 0000000..62d4295 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/commands/ExitCommand.java @@ -0,0 +1,29 @@ +package ru.ifmo.cli_application.commands; + +import java.util.List; + +import ru.ifmo.cli_application.Context; +import ru.ifmo.cli_application.tokens.IExecutable; +import ru.ifmo.cli_application.tokens.IToken; + +/** + * Class for exit CLI application command + */ +public class ExitCommand implements IToken, IExecutable { + private Context ctx; + + public ExitCommand(Context ctx) { + this.ctx = ctx; + } + + @Override + public String execute(List args, String inputStream) { + ctx.exit(); + return ""; + } + + @Override + public String getValue() { + return "exit"; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/commands/PwdCommand.java b/src/main/java/ru/ifmo/cli_application/commands/PwdCommand.java new file mode 100644 index 0000000..2fc4bf1 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/commands/PwdCommand.java @@ -0,0 +1,21 @@ +package ru.ifmo.cli_application.commands; + +import java.util.List; + +import ru.ifmo.cli_application.tokens.IExecutable; +import ru.ifmo.cli_application.tokens.IToken; + +/** + * Class for showing current working directory command + */ +public class PwdCommand implements IToken, IExecutable { + @Override + public String execute(List args, String inputStream) { + return System.getProperty("user.dir"); + } + + @Override + public String getValue() { + return "pwd"; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/commands/WcCommand.java b/src/main/java/ru/ifmo/cli_application/commands/WcCommand.java new file mode 100644 index 0000000..2ff4a56 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/commands/WcCommand.java @@ -0,0 +1,82 @@ +package ru.ifmo.cli_application.commands; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +import ru.ifmo.cli_application.tokens.IExecutable; +import ru.ifmo.cli_application.tokens.IToken; + +/** + * Class for calculation of lines, words and bytes count for each file + */ +public class WcCommand implements IToken, IExecutable { + @Override + public String execute(List args, String inputStream) { + if (args.size() == 0) { + Stats inputStreamStats = new Stats(null, inputStream); + return inputStreamStats.statisticWithoutFileName(); + } + List stats = new ArrayList<>(); + for (IToken arg : args) { + try (Scanner scanner = new Scanner(new FileInputStream(arg.getValue()))) { + StringBuilder fileContent = new StringBuilder(); + while (scanner.hasNextLine()) { + fileContent.append(scanner.nextLine()); + fileContent.append('\n'); + } + stats.add(new Stats(arg.getValue(), fileContent.toString())); + } catch (FileNotFoundException e) { + return "File not found: " + arg.getValue(); + } + } + if (stats.size() == 1) { + return stats.get(0).statisticWithFileName(); + } + stats.add(new Stats(stats)); + StringBuilder stringBuilder = new StringBuilder(); + for (Stats stat : stats) { + stringBuilder.append(stat.statisticWithFileName()); + stringBuilder.append('\n'); + } + return stringBuilder.toString(); + } + + @Override + public String getValue() { + return "wc"; + } + + private class Stats { + private long linesCount; + private long wordsCount; + private long bytesCount; + private String fileName; + + Stats(List stats) { + fileName = "total"; + for (Stats stat : stats) { + this.linesCount += stat.linesCount; + this.wordsCount += stat.wordsCount; + this.bytesCount += stat.bytesCount; + } + } + + Stats(String fileName, String string) { + linesCount = string.chars().filter(ch -> ch == '\n').count() + 1; + wordsCount = string.split("[ \n\t\r]+").length; + bytesCount = string.getBytes().length; + this.fileName = fileName; + } + + public String statisticWithoutFileName() { + return String.format("%d\t%d\t%d", linesCount, wordsCount, bytesCount); + } + + public String statisticWithFileName() { + return String.format("%d\t%d\t%d\t%s", linesCount, wordsCount, bytesCount, fileName); + } + } +} diff --git a/src/main/java/ru/ifmo/cli_application/parser/Parser.java b/src/main/java/ru/ifmo/cli_application/parser/Parser.java new file mode 100644 index 0000000..752e88f --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/parser/Parser.java @@ -0,0 +1,54 @@ +package ru.ifmo.cli_application.parser; + +import java.util.ArrayList; +import java.util.List; + +import ru.ifmo.cli_application.tokens.IToken; + +/** + * Parses string to {@link List} using {@link ParserRule} + */ +public abstract class Parser { + private List rules; + + public Parser() { + this.rules = new ArrayList<>(); + } + + /** + * Method to add new parsing rule + * @param parserRule + */ + public void addParsingRule(ParserRule parserRule) { + this.rules.add(parserRule); + } + + /** + * Function that split command string to list of string tokens and then, parse each token using {@link ParserRule} + * @param command + * @return List of parsed tokens + */ + public List parseTokens(String command) { + List splitedCommand = commandPreprocessing(command); + List outputTokens = new ArrayList<>(splitedCommand.size()); + for (String commandToken : splitedCommand) { + if (commandToken.isEmpty()) { + continue; + } + for (ParserRule rule : rules) { + if (rule.getPredicate().apply(commandToken, outputTokens)) { + outputTokens.add(rule.getConstructor().apply(commandToken)); + break; + } + } + } + return outputTokens; + } + + /** + * Function for split and preprocess command + * @param command + * @return {@link List} of preprocessed command + */ + public abstract List commandPreprocessing(String command); +} diff --git a/src/main/java/ru/ifmo/cli_application/parser/ParserRule.java b/src/main/java/ru/ifmo/cli_application/parser/ParserRule.java new file mode 100644 index 0000000..32d7fb7 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/parser/ParserRule.java @@ -0,0 +1,28 @@ +package ru.ifmo.cli_application.parser; + +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +import ru.ifmo.cli_application.tokens.IToken; + +/** + * Rule for parsing string tokens to IToken + */ +public class ParserRule { + private BiFunction, Boolean> predicate; + private Function constructor; + + public ParserRule(BiFunction, Boolean> predicate, Function constructor) { + this.predicate = predicate; + this.constructor = constructor; + } + + public BiFunction, Boolean> getPredicate() { + return predicate; + } + + public Function getConstructor() { + return constructor; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/parser/SimpleParser.java b/src/main/java/ru/ifmo/cli_application/parser/SimpleParser.java new file mode 100644 index 0000000..6faee67 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/parser/SimpleParser.java @@ -0,0 +1,70 @@ +package ru.ifmo.cli_application.parser; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import ru.ifmo.cli_application.Context; +import ru.ifmo.cli_application.commands.*; +import ru.ifmo.cli_application.tokens.*; + +/** + * Parser implementation for our CLI application + */ +public class SimpleParser extends Parser { + private static Pattern splittingPattern = Pattern.compile("[^\\s\"']+|\"[^\"]*\"|'[^']*'"); + private Context ctx; + + public SimpleParser(Context ctx) { + this.ctx = ctx; + addParsingRules(); + } + + private void addParsingRules() { + List predicates = new ArrayList<>(); + Function, Boolean> isCommand = (List t) -> t.size() == 0 || t.get(t.size() - 1) instanceof IDelimiter; + predicates.add(new ParserRule((String s, List t) -> s.startsWith("\'") && s.endsWith("\'"), FullyQuoted::new)); + predicates.add(new ParserRule((String s, List t) -> s.startsWith("\"") && s.endsWith("\""), (String s) -> new Quoted(this, s))); + predicates.add(new ParserRule((String s, List t) -> s.startsWith("$"), (String s) -> new Variable(s, ctx))); + predicates.add(new ParserRule((String s, List t) -> s.equals("|"), (String s) -> new Pipe())); + predicates.add(new ParserRule((String s, List t) -> s.contains("="), (String s) -> new AssigningOperator(s, ctx))); + predicates.add(new ParserRule((String s, List t) -> isCommand.apply(t) && s.equals("cat"), (String s) -> new CatCommand())); + predicates.add(new ParserRule((String s, List t) -> isCommand.apply(t) && s.equals("echo"), (String s) -> new EchoCommand())); + predicates.add(new ParserRule((String s, List t) -> isCommand.apply(t) && s.equals("pwd"), (String s) -> new PwdCommand())); + predicates.add(new ParserRule((String s, List t) -> isCommand.apply(t) && s.equals("wc"), (String s) -> new WcCommand())); + predicates.add(new ParserRule((String s, List t) -> isCommand.apply(t) && s.equals("exit"), (String s) -> new ExitCommand(ctx))); + predicates.add(new ParserRule((String s, List t) -> isCommand.apply(t), Command::new)); + predicates.add(new ParserRule((String s, List t) -> true, Argument::new)); + for (ParserRule p : predicates) { + super.addParsingRule(p); + } + } + + public List commandPreprocessing(String command) { + Matcher matcher = splittingPattern.matcher(command); + List list = new ArrayList<>(); + while (matcher.find()) { + list.add(matcher.group()); + } + List finalList = new ArrayList<>(); + for (String elem : list) { + if (elem.contains("$") && !elem.startsWith("\'") && !elem.endsWith("\'")) { + String[] splitedElem = elem.split("\\$"); + if (elem.startsWith("$")) { + splitedElem[0] = new Variable("$" + splitedElem[0], ctx).getValue(); + } + for (int i = 1; i < splitedElem.length; i++) { + splitedElem[i] = new Variable("$" + splitedElem[i], ctx).getValue(); + } + finalList.add(Arrays.stream(splitedElem).collect(Collectors.joining(""))); + } else { + finalList.add(elem); + } + } + return finalList; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/tokens/Argument.java b/src/main/java/ru/ifmo/cli_application/tokens/Argument.java new file mode 100644 index 0000000..8856b01 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/tokens/Argument.java @@ -0,0 +1,17 @@ +package ru.ifmo.cli_application.tokens; + +/** + * Command`s argument + */ +public class Argument implements IToken { + private String value; + + public Argument(String value) { + this.value = value; + } + + @Override + public String getValue() { + return this.value; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/tokens/AssigningOperator.java b/src/main/java/ru/ifmo/cli_application/tokens/AssigningOperator.java new file mode 100644 index 0000000..aec6114 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/tokens/AssigningOperator.java @@ -0,0 +1,37 @@ +package ru.ifmo.cli_application.tokens; + +import java.util.List; + +import ru.ifmo.cli_application.Context; + +/** + * Class for assigning operator execution + */ +public class AssigningOperator implements IToken, IExecutable { + private String rawString; + private Context ctx; + + public AssigningOperator(String s, Context ctx) { + this.rawString = s; + if (!this.rawString.contains("=")) { + throw new IllegalArgumentException("first argument should contains '='"); + } + if (ctx == null) { + throw new NullPointerException("context should be initialized"); + } + this.ctx = ctx; + } + + + @Override + public String execute(List args, String inputStream) { + String[] splitedString = rawString.split("="); + ctx.setVariable(splitedString[0], splitedString[1]); + return ""; + } + + @Override + public String getValue() { + return "="; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/tokens/FullyQuoted.java b/src/main/java/ru/ifmo/cli_application/tokens/FullyQuoted.java new file mode 100644 index 0000000..30dae6d --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/tokens/FullyQuoted.java @@ -0,0 +1,16 @@ +package ru.ifmo.cli_application.tokens; + +/** + * Class for fully quoted token + */ +public class FullyQuoted implements IToken { + private String string; + + public FullyQuoted(String string) { + this.string = string.substring(1, string.length() - 1); + } + + public String getValue() { + return string; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/tokens/IDelimiter.java b/src/main/java/ru/ifmo/cli_application/tokens/IDelimiter.java new file mode 100644 index 0000000..8e2d5e4 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/tokens/IDelimiter.java @@ -0,0 +1,7 @@ +package ru.ifmo.cli_application.tokens; + +/** + * Flag interface for delimiter tokens + */ +public interface IDelimiter { +} diff --git a/src/main/java/ru/ifmo/cli_application/tokens/IExecutable.java b/src/main/java/ru/ifmo/cli_application/tokens/IExecutable.java new file mode 100644 index 0000000..e9afac9 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/tokens/IExecutable.java @@ -0,0 +1,17 @@ +package ru.ifmo.cli_application.tokens; + +import java.util.List; + +/** + * Interface of executable commands + */ +public interface IExecutable { + /** + * Function to execute command + * + * @param args command arguments + * @param inputStream result of previous commands execution in pipe + * @return result of command execution + */ + String execute(List args, String inputStream); +} diff --git a/src/main/java/ru/ifmo/cli_application/tokens/IToken.java b/src/main/java/ru/ifmo/cli_application/tokens/IToken.java new file mode 100644 index 0000000..8c82f22 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/tokens/IToken.java @@ -0,0 +1,12 @@ +package ru.ifmo.cli_application.tokens; + +/** + * Interface for all command tokens + */ +public interface IToken { + /** + * Getter for string interpretation of token + * @return + */ + String getValue(); +} diff --git a/src/main/java/ru/ifmo/cli_application/tokens/Pipe.java b/src/main/java/ru/ifmo/cli_application/tokens/Pipe.java new file mode 100644 index 0000000..c550f9b --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/tokens/Pipe.java @@ -0,0 +1,8 @@ +package ru.ifmo.cli_application.tokens; + +public class Pipe implements IToken, IDelimiter { + @Override + public String getValue() { + return "|"; + } +} diff --git a/src/main/java/ru/ifmo/cli_application/tokens/Quoted.java b/src/main/java/ru/ifmo/cli_application/tokens/Quoted.java new file mode 100644 index 0000000..bc272c1 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/tokens/Quoted.java @@ -0,0 +1,26 @@ +package ru.ifmo.cli_application.tokens; + +import java.util.List; + +import ru.ifmo.cli_application.parser.Parser; + +public class Quoted implements IToken { + private String string; + private Parser parser; + + public Quoted(Parser parser, String string) { + this.parser = parser; + this.string = string.substring(1, string.length() - 1); + } + + @Override + public String getValue() { + List tokens = parser.parseTokens(this.string); + StringBuilder result = new StringBuilder(); + for (IToken token : tokens) { + result.append(token.getValue()); + result.append(" "); + } + return result.toString(); + } +} diff --git a/src/main/java/ru/ifmo/cli_application/tokens/Variable.java b/src/main/java/ru/ifmo/cli_application/tokens/Variable.java new file mode 100644 index 0000000..9f69e83 --- /dev/null +++ b/src/main/java/ru/ifmo/cli_application/tokens/Variable.java @@ -0,0 +1,17 @@ +package ru.ifmo.cli_application.tokens; + +import ru.ifmo.cli_application.Context; + +public class Variable implements IToken { + String variable; + Context ctx; + + public Variable(String var, Context ctx) { + this.variable = var; + this.ctx = ctx; + } + + public String getValue() { + return ctx.getVariable(variable.substring(1)); + } +} diff --git a/src/test/java/ru/ifmo/cli_application/commands/EchoCommandTest.java b/src/test/java/ru/ifmo/cli_application/commands/EchoCommandTest.java new file mode 100644 index 0000000..a812638 --- /dev/null +++ b/src/test/java/ru/ifmo/cli_application/commands/EchoCommandTest.java @@ -0,0 +1,20 @@ +package ru.ifmo.cli_application.commands; + +import org.junit.Test; +import ru.ifmo.cli_application.tokens.FullyQuoted; +import ru.ifmo.cli_application.tokens.IToken; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class EchoCommandTest { + @Test + public void testEcho() { + EchoCommand echoCommand = new EchoCommand(); + List arguments = Arrays.asList(new FullyQuoted[]{new FullyQuoted("\"hello\"")}); + String result = echoCommand.execute(arguments, ""); + assertEquals("hello ", result); + } +} diff --git a/src/test/java/ru/ifmo/cli_application/commands/ExitCommandTest.java b/src/test/java/ru/ifmo/cli_application/commands/ExitCommandTest.java new file mode 100644 index 0000000..3d0def6 --- /dev/null +++ b/src/test/java/ru/ifmo/cli_application/commands/ExitCommandTest.java @@ -0,0 +1,16 @@ +package ru.ifmo.cli_application.commands; + +import org.junit.Test; +import ru.ifmo.cli_application.Context; + +import static org.junit.Assert.assertTrue; + +public class ExitCommandTest { + @Test + public void testExit() { + Context ctx = new Context(); + ExitCommand exitCommand = new ExitCommand(ctx); + exitCommand.execute(null, null); + assertTrue(ctx.isClosed()); + } +} diff --git a/src/test/java/ru/ifmo/cli_application/commands/WcCommandTest.java b/src/test/java/ru/ifmo/cli_application/commands/WcCommandTest.java new file mode 100644 index 0000000..86a8be3 --- /dev/null +++ b/src/test/java/ru/ifmo/cli_application/commands/WcCommandTest.java @@ -0,0 +1,16 @@ +package ru.ifmo.cli_application.commands; + +import org.junit.Test; + +import java.util.ArrayList; + +import static org.junit.Assert.assertEquals; + +public class WcCommandTest { + @Test + public void testWcWithoutArgs() { + WcCommand command = new WcCommand(); + String result = command.execute(new ArrayList<>(), "one two three"); + assertEquals("1\t3\t13", result); + } +} diff --git a/src/test/java/ru/ifmo/cli_application/full_tests/FullTest.java b/src/test/java/ru/ifmo/cli_application/full_tests/FullTest.java new file mode 100644 index 0000000..f35e91f --- /dev/null +++ b/src/test/java/ru/ifmo/cli_application/full_tests/FullTest.java @@ -0,0 +1,42 @@ +package ru.ifmo.cli_application.full_tests; + +import java.io.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +import org.junit.Test; +import ru.ifmo.cli_application.Application; + +import static org.junit.Assert.assertEquals; + +public class FullTest { + private String executeCommand(List command) throws IOException { + List commands = new ArrayList<>(command); + commands.add("exit"); + InputStream inputStream = new ByteArrayInputStream(commands.stream().collect(Collectors.joining("\n")).getBytes()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Application app = new Application(inputStream, outputStream); + app.start(); + return new String(outputStream.toByteArray()); + } + + @Test + public void echoVariableTest() throws IOException { + assertEquals("\n12 \n\n", executeCommand(Arrays.asList("x=12", "echo $x"))); + } + + @Test + public void echoVariableConcatTest() throws IOException { + assertEquals("\n1212 \n\n", executeCommand(Arrays.asList("x=12", "echo 12$x"))); + } + + @Test + public void echoFromCommand() throws IOException { + assertEquals("\n\n12 \n\n", executeCommand(Arrays.asList("x=ec", "y=ho", "$x$y 12"))); + } + +} diff --git a/src/test/java/ru/ifmo/cli_application/parser/SimpleParserTest.java b/src/test/java/ru/ifmo/cli_application/parser/SimpleParserTest.java new file mode 100644 index 0000000..d063a4a --- /dev/null +++ b/src/test/java/ru/ifmo/cli_application/parser/SimpleParserTest.java @@ -0,0 +1,86 @@ +package ru.ifmo.cli_application.parser; + +import org.junit.Test; +import ru.ifmo.cli_application.Context; +import ru.ifmo.cli_application.commands.CatCommand; +import ru.ifmo.cli_application.commands.EchoCommand; +import ru.ifmo.cli_application.tokens.*; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class SimpleParserTest { + @Test + public void testParsingOneInnerCommand() { + Context ctx = new Context(); + Parser parser = new SimpleParser(ctx); + List tokens = parser.parseTokens("echo"); + assertEquals(1, tokens.size()); + assertTrue(tokens.get(0) instanceof EchoCommand); + } + + @Test + public void testParsingInnerCommandWithArgs() { + Context ctx = new Context(); + Parser parser = new SimpleParser(ctx); + List tokens = parser.parseTokens("echo 123"); + assertEquals(2, tokens.size()); + assertTrue(tokens.get(0) instanceof EchoCommand); + assertTrue(tokens.get(1) instanceof Argument); + } + + @Test + public void testParsingCommandsWithPipe() { + Context ctx = new Context(); + Parser parser = new SimpleParser(ctx); + List tokens = parser.parseTokens("echo 123 | cat"); + assertEquals(4, tokens.size()); + assertTrue(tokens.get(0) instanceof EchoCommand); + assertTrue(tokens.get(1) instanceof Argument); + assertTrue(tokens.get(2) instanceof Pipe); + assertTrue(tokens.get(3) instanceof CatCommand); + } + + + @Test + public void testParsingCommandWithQuotedArg() { + Context ctx = new Context(); + Parser parser = new SimpleParser(ctx); + List tokens = parser.parseTokens("echo \"123 | cat\""); + assertEquals(2, tokens.size()); + assertTrue(tokens.get(0) instanceof EchoCommand); + assertTrue(tokens.get(1) instanceof Quoted); + } + + @Test + public void testParsingCommandWithFullyQuotedArg() { + Context ctx = new Context(); + Parser parser = new SimpleParser(ctx); + List tokens = parser.parseTokens("echo \'123 | cat\'"); + assertEquals(2, tokens.size()); + assertTrue(tokens.get(0) instanceof EchoCommand); + assertTrue(tokens.get(1) instanceof FullyQuoted); + } + + @Test + public void testParsingCommandWithFullyQuotedArgContainsQuote() { + Context ctx = new Context(); + Parser parser = new SimpleParser(ctx); + List tokens = parser.parseTokens("echo \'1\" | 2\'"); + assertEquals(2, tokens.size()); + assertTrue(tokens.get(0) instanceof EchoCommand); + assertTrue(tokens.get(1) instanceof FullyQuoted); + } + + @Test + public void testParsingCommandWithFullyQuotedArgWithDollar() { + Context ctx = new Context(); + Parser parser = new SimpleParser(ctx); + List tokens = parser.parseTokens("echo \'$x\'"); + assertEquals(2, tokens.size()); + assertTrue(tokens.get(0) instanceof EchoCommand); + assertTrue(tokens.get(1) instanceof FullyQuoted); + } +} diff --git a/src/test/java/ru/ifmo/cli_application/tokens/AssigningOperatorTest.java b/src/test/java/ru/ifmo/cli_application/tokens/AssigningOperatorTest.java new file mode 100644 index 0000000..836d16d --- /dev/null +++ b/src/test/java/ru/ifmo/cli_application/tokens/AssigningOperatorTest.java @@ -0,0 +1,23 @@ +package ru.ifmo.cli_application.tokens; + +import org.junit.Test; +import ru.ifmo.cli_application.Context; + +import static org.junit.Assert.assertEquals; + +public class AssigningOperatorTest { + @Test + public void testAssigning() { + Context context = new Context(); + + AssigningOperator operator = new AssigningOperator("h=5", context); + operator.execute(null, null); + assertEquals("5", context.getVariable("h")); + } + + @Test(expected = IllegalArgumentException.class) + public void testAssigningWithStringWithoutEquals() { + Context context = new Context(); + AssigningOperator operator = new AssigningOperator("h5", context); + } +}