diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a00c9f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+/.idea
+/out
+
+# Ignore Gradle project-specific cache directory
+.gradle
+
+# Ignore Gradle build output directory
+build
diff --git a/README.md b/README.md
index ba70ce9..e69c575 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,13 @@
-### Команда №3 (MSE'23)
-- Васильев Анатолий
-- Васильев Максим
-- Мензоров Константин
-- Золотухин Илья
-
-### Цель работы
-
-**Требования и нюансы bash**:
-- команды `cat`, `echo`, `wc`, `pwd`, `exit`
-- 'одинарные' и "двойные" кавычки (full and weak quoting)
-- команды вида `var=value`, оператор `$var`
-- вызов внешних программ
-- пайплайны, т.е. оператор `|`
+Команда #2 добавила команды `cd`, `ls` и юнит-тесты, а также архитектурный ревью к команде #3
+
+# Ревью архитектуры
+
+Архитектура в целом монолитная - все состояние приложения держится в `main`. В рамках данной задачи это работает, но могут возникнуть трудности при необходимости вычислений со своим контекстом (запуск подпрограмм).
+
+Из хорошего: разбор пользовательского ввода на команды вынесен в отдельный модуль, добавление новых команд для разбора выполняется легко. Но выразительности недостаточно, чтобы указать количество/формат параметров, принемаемых командой. Поэтому проверкой правильности аргументов выполняется внутри класса команды в методе `start`.
+
+Несмотря на то, что есть класс `PipeManagerCommands`, который по всей видимости должен заниматься управлением пайпами, по факту за вызов следующей команды в пайпе ответственна именно текущая команда. Т. е. механизм пайпов абстрагирован не лучшим образом.
+
+Добавление команды `cd` вышло достаточно трудоемким, поскольку все команды были завязаны на текущую директорию jvm, которую менять нельзя. Поэтому пришлось внести модификации в каждую команду, которая работает с файловой системой.
+
+Добавление же команды `ls` не вызвало особых трудностей. Достаточно было создать новый класс `Ls`, реализовать логику и проверку аргументов, и "зарегистрировать" команду в глобальном словаре команд.
diff --git a/SD.iml b/SD.iml
new file mode 100644
index 0000000..c90834f
--- /dev/null
+++ b/SD.iml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SD.png b/SD.png
deleted file mode 100644
index 6eb2b29..0000000
Binary files a/SD.png and /dev/null differ
diff --git a/arch_docs.md b/arch_docs.md
deleted file mode 100644
index 21e22ec..0000000
--- a/arch_docs.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# Блок-схема работы интерпретатора командной строки
-
-Блок-схема, представленная на изображении, иллюстрирует работу интерпретатора командной строки.
-
-
-
-## Блок "Запуск программы":
-
-Загрузка интерпретатора командной строки.
-
-Инициализация переменных и системных ресурсов.
-
-## Блок "Ввод пользователя":
-
-Интерпретатор выводит приглашение (>).
-
-Интерпретатор ожидает ввод команды пользователя.
-
-## Блок "Парсер":
-
-Разбивает введенную строку на отдельные токены (команды, аргументы, опции).
-
-Формирует список токенов для дальнейшего анализа.
-
-## Блок "Синтаксический анализатор":
-
-Проверяет, является ли последовательность токенов корректной командой.
-
-Проверяет правильность использования аргументов и опций.
-
-Выводит сообщение об ошибке, если команда некорректна.
-
-## Блок "Семантический анализатор":
-
-Проверяет, существуют ли команды, аргументы и опции, указанные в команде.
-
-Проверяет права доступа к файлам и устройствам, указанным в команде.
-
-Проверяет корректность программы с точки зрения типов.
-
-Выводит сообщение об ошибке, если команда семантически некорректна.
-
-## Блок "AST дерево":
-
-Из токенов строится AST дерево.
-
-## Блок "exit?":
-
-Проверяет программа == or != exit.
-
-Переходит в блок "Исполнение программы", если ==, иначе в "Завершение работы"
-
-## Блок "Исполнение программы":
-
-В процессе исполнения программы происходит выполнение инструкций, представленных в AST.
-
-Это включает в себя вычисление выражений, вызов функций, управление потоком выполнения и взаимодействие с памятью и внешними ресурсами.
-
-Исполнение программы продолжается до тех пор, пока не будет достигнут конечный узел AST.
-
-## Блок "Завершение работы":
-
-Интерпретатор освобождает ресурсы, использованные при выполнении команды и завершает работу.
-
-# Дополнительные элементы:
-
-## Блок "Базовый класс command":
-
-Базовый класс для всех команд.
-
-## Блок "Наследники":
-
-Классы, наследующие от базового класса command и реализующие конкретные команды.
-
-## Блок "Словарь с информацией об окружении":
-
-Содержит информацию о текущем каталоге, переменных окружения и т.д.
-
-## Блок "Словарь с переменными":
-
-Содержит значения переменных, заданных пользователем.
-
-## Блок "Штатная информация":
-
-Содержит информацию о коде завершения программы.
-
-## Блок "exit?":
-
-Используется для проверки, требуется ли завершение работы интерпретатора.
-
-Интерпретатор завершает работу, если пользователь ввел команду exit, или она получена иными способами.
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..80697ce
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,40 @@
+plugins {
+ id "java"
+ id 'application'
+}
+
+group = "mse.sd.bash.analyze"
+version = "1.0-SNAPSHOT"
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation("org.jetbrains:annotations:16.0.2")
+ testImplementation(platform("org.junit:junit-bom:5.7.0"))
+ testImplementation("org.junit.jupiter:junit-jupiter")
+}
+
+java {
+ sourceSets {
+ main {
+ java {
+ srcDirs = ["src/main"]
+ }
+ }
+ test {
+ java {
+ srcDirs = ["src/test"]
+ }
+ }
+ }
+}
+
+tasks.compileJava {
+ options.release.set(17)
+}
+
+tasks.test {
+ useJUnitPlatform()
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7f93135
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..3fa8f86
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..1aa94a4
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# 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 ;; #(
+ MSYS* | 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
+ if ! command -v java >/dev/null 2>&1
+ then
+ 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
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..6689b85
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@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=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@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="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+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 execute
+
+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
+
+: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 %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 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!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/project/.idea/workspace.xml b/project/.idea/workspace.xml
new file mode 100644
index 0000000..8b19098
--- /dev/null
+++ b/project/.idea/workspace.xml
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "lastFilter": {
+ "state": "OPEN",
+ "assignee": "IIlya-ipti"
+ }
+}
+
+
+
+
+
+
+ {
+ "associatedIndex": 6
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1709935731232
+
+
+ 1709935731232
+
+
+
+
+
+
+
+
+ 1709978629522
+
+
+
+ 1709978629522
+
+
+
+ 1709979804576
+
+
+
+ 1709979804576
+
+
+
+ 1709980538347
+
+
+
+ 1709980538347
+
+
+
+ 1709992166003
+
+
+
+ 1709992166003
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/project/out/production/project/.idea/workspace.xml b/project/out/production/project/.idea/workspace.xml
new file mode 100644
index 0000000..add0713
--- /dev/null
+++ b/project/out/production/project/.idea/workspace.xml
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "lastFilter": {
+ "state": "OPEN",
+ "assignee": "IIlya-ipti"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "associatedIndex": 6
+}
+
+
+
+
+
+ {
+ "keyToString": {
+ "Application.Main.executor": "Run",
+ "RunOnceActivity.OpenProjectViewOnStart": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "git-widget-placeholder": "Rebasing hw-2-dev-Tolik",
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "C:/Users/Ilya/IdeaProjects/SD/project",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "vue.rearranger.settings.migration": "true"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1709935731232
+
+
+ 1709935731232
+
+
+
+
+
+
+
+ 1709978629522
+
+
+
+ 1709978629522
+
+
+
+ 1709979804576
+
+
+
+ 1709979804576
+
+
+
+ 1709980538347
+
+
+
+ 1709980538347
+
+
+
+ 1709992166003
+
+
+
+ 1709992166003
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..cede0a5
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = "SD"
\ No newline at end of file
diff --git a/src/main/mse/sd/bash/Main.java b/src/main/mse/sd/bash/Main.java
new file mode 100644
index 0000000..df4bde8
--- /dev/null
+++ b/src/main/mse/sd/bash/Main.java
@@ -0,0 +1,51 @@
+package mse.sd.bash;
+
+import mse.sd.bash.analyze.Parser;
+import mse.sd.bash.commands.Command;
+import mse.sd.bash.commands.PipeManagerCommands;
+import mse.sd.bash.commands.Pwd;
+import mse.sd.bash.commands.Cd;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.Scanner;
+
+public class Main {
+ private static String cwd = System.getProperty("user.dir");
+
+ private static void testMain(String rs) throws IOException, IllegalArgumentException {
+ Parser parser = new Parser(rs);
+ parser.parse();
+ Command[][] commands = parser.getCommands();
+ String[][][] commandsArgs = parser.getCommandsArgs();
+
+ // by "&&"
+ for (int i = 0; i < commands.length; i++) {
+ PipeManagerCommands pipeManagerCommands =
+ new PipeManagerCommands(commands[i],commandsArgs[i], cwd);
+ pipeManagerCommands.start();
+ if (commands[i][0] instanceof Cd) {
+ Cd cd = (Cd)commands[i][0];
+ if (cd.newCwd != null) {
+ cwd = cd.newCwd;
+ }
+ }
+ }
+ }
+
+ public static void main(String[] args) throws IllegalArgumentException {
+ while (true) {
+ System.out.print(cwd + ">");
+ Scanner sc = new Scanner(System.in);
+ try {
+ testMain(sc.nextLine());
+ System.out.println();
+ } catch (IOException | IllegalArgumentException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/mse/sd/bash/analyze/CommandParser.java b/src/main/mse/sd/bash/analyze/CommandParser.java
new file mode 100644
index 0000000..f2b9ce0
--- /dev/null
+++ b/src/main/mse/sd/bash/analyze/CommandParser.java
@@ -0,0 +1,78 @@
+package mse.sd.bash.analyze;
+
+import mse.sd.bash.commands.*;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public class CommandParser
+{
+ private static final Map CMD_MAP = new HashMap<>();
+
+ static {
+ CMD_MAP.put("cat", new Cat());
+ CMD_MAP.put("echo", new Echo());
+ CMD_MAP.put("exit", new Exit());
+ CMD_MAP.put("pwd", new Pwd());
+ CMD_MAP.put("wc", new Wc());
+ CMD_MAP.put("cd", new Cd());
+ CMD_MAP.put("ls", new Ls());
+ }
+
+ static private boolean isCommand(String[] str)
+ {
+ if(str.length > 0)
+ {
+ return CMD_MAP.containsKey(str[0]);
+ }
+ return false;
+ }
+ static public Command parse(String[] str)
+ {
+ if(isCommand(str))
+ {
+ return CMD_MAP.get(str[0]).getNew();
+ }
+ throw new IllegalArgumentException("unexpected command : " + Arrays.toString(str));
+ }
+
+ static public Command[] parse(String[][] str)
+ {
+ return Arrays.stream(str)
+ .map(CommandParser::parse)
+ .toList()
+ .toArray(new Command[0]);
+ }
+
+ static public Command[][] parse(String[][][] str)
+ {
+ return Arrays.stream(str)
+ .map(CommandParser::parse)
+ .toList()
+ .toArray(new Command[0][0]);
+ }
+
+ static public String[] getArgs(String[] args)
+ {
+ return Arrays.copyOfRange(args,1,args.length);
+ }
+
+ static public String[][] getArgs(String [][]args)
+ {
+ return Arrays.stream(args)
+ .map(CommandParser::getArgs)
+ .toList()
+ .toArray(new String[0][0]);
+ }
+
+ static public String[][][] getArgs(String [][][]args)
+ {
+ return Arrays.stream(args)
+ .map(CommandParser::getArgs)
+ .toList()
+ .toArray(new String[0][0][0]);
+ }
+
+}
diff --git a/src/main/mse/sd/bash/analyze/Parser.java b/src/main/mse/sd/bash/analyze/Parser.java
new file mode 100644
index 0000000..c64c8ba
--- /dev/null
+++ b/src/main/mse/sd/bash/analyze/Parser.java
@@ -0,0 +1,37 @@
+package mse.sd.bash.analyze;
+
+import mse.sd.bash.commands.*;
+
+public class Parser {
+ private final String source;
+ private Command[][] commands = null;
+ private String[][][] commandsArgs = null;
+
+ public String[][][] getCommandsArgs()
+ {
+ return this.commandsArgs;
+ }
+
+ public Command[][] getCommands()
+ {
+ return this.commands;
+ }
+
+ public Parser(String source) {
+ this.source = source;
+ }
+
+ private final SplitterString splitBySpace = new SplitterString(" ");
+ private final SplitterString splitByLine = new SplitterString("\\|");
+ private final SplitterString splitByAmpersand = new SplitterString("&&");
+
+ // разбор строки
+ public void parse() {
+ String[][][] postProcessing = splitBySpace
+ .split(splitByLine
+ .split(splitByAmpersand
+ .split(this.source)));
+ this.commands = CommandParser.parse(postProcessing);
+ this.commandsArgs = CommandParser.getArgs(postProcessing);
+ }
+}
diff --git a/src/main/mse/sd/bash/analyze/SplitterString.java b/src/main/mse/sd/bash/analyze/SplitterString.java
new file mode 100644
index 0000000..0411835
--- /dev/null
+++ b/src/main/mse/sd/bash/analyze/SplitterString.java
@@ -0,0 +1,40 @@
+package mse.sd.bash.analyze;
+
+import java.util.Arrays;
+
+public class SplitterString {
+ String del_ = " ";
+ SplitterString(String input)
+ {
+ this.del_ = input;
+ }
+
+ public String [] split(String input)
+ {
+ // split and clean
+ return Arrays.stream(input.split(del_))
+ .map(String::trim)
+ .map(str -> str.replaceAll("\\s+"," "))
+ .toList()
+ .toArray(new String[0]);
+ }
+
+ public String[][] split(String[] input)
+ {
+ return Arrays.stream(input)
+ .map(this::split)
+ .filter(str -> str.length > 0)
+ .toList()
+ .toArray(new String[0][0]);
+ }
+
+ public String[][][] split(String[][] input)
+ {
+ return Arrays.stream(input)
+ .map(this::split)
+ .filter(str -> str.length > 0)
+ .toList()
+ .toArray(new String[0][0][0]);
+ }
+
+}
diff --git a/src/main/mse/sd/bash/commands/Cat.java b/src/main/mse/sd/bash/commands/Cat.java
new file mode 100644
index 0000000..02142bb
--- /dev/null
+++ b/src/main/mse/sd/bash/commands/Cat.java
@@ -0,0 +1,49 @@
+package mse.sd.bash.commands;
+import java.io.*;
+import java.nio.file.*;
+
+public class Cat extends Command {
+ @Override
+ public void eval(Reader reader) {
+ StringBuilder result = new StringBuilder();
+ try (BufferedReader BufReader = new BufferedReader(reader)) {
+ String line;
+ while ((line = BufReader.readLine()) != null) {
+ result.append(line).append("\n");
+ }
+ if(nextCommand != null)
+ {
+ nextCommand.eval(new StringReader(result.toString()));
+ }
+ else
+ {
+ System.out.println(result);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ @Override
+ public void start() {
+ try {
+ Path inputPath = Paths.get(args[0]);
+ if (!inputPath.isAbsolute()) {
+ inputPath = Paths.get(cwd).resolve(inputPath);
+ }
+ File f = inputPath.toFile();
+ if (!f.exists() || f.isDirectory()) {
+ throw new IllegalArgumentException();
+ }
+ eval(new FileReader(f));
+ } catch (Exception e) {
+ throw new IllegalArgumentException("error args");
+ }
+ }
+
+ @Override
+ public Cat getNew() {
+ return new Cat();
+ }
+}
diff --git a/src/main/mse/sd/bash/commands/Cd.java b/src/main/mse/sd/bash/commands/Cd.java
new file mode 100644
index 0000000..bf751c8
--- /dev/null
+++ b/src/main/mse/sd/bash/commands/Cd.java
@@ -0,0 +1,43 @@
+package mse.sd.bash.commands;
+
+import java.io.*;
+import java.nio.file.*;
+
+public class Cd extends Command {
+ public String newCwd = null;
+
+ @Override
+ public void eval(Reader reader) throws IOException {
+ try (BufferedReader bufferedReader = new BufferedReader(reader)) {
+ newCwd = Paths.get(bufferedReader.readLine()).normalize().toString();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void start() throws IOException {
+ if (args.length == 0) {
+ System.out.println(cwd);
+ return;
+ }
+ try {
+ Path inputPath = Paths.get(args[0]);
+ if (!inputPath.isAbsolute()) {
+ inputPath = Paths.get(cwd).resolve(inputPath);
+ }
+ File f = inputPath.toFile();
+ if (!f.exists() || !f.isDirectory()) {
+ throw new IllegalArgumentException();
+ }
+ eval(new StringReader(inputPath.toString()));
+ } catch (Exception e) {
+ throw new IllegalArgumentException("error args");
+ }
+ }
+
+ @Override
+ public Command getNew() {
+ return new Cd();
+ }
+}
diff --git a/src/main/mse/sd/bash/commands/Command.java b/src/main/mse/sd/bash/commands/Command.java
new file mode 100644
index 0000000..4d52379
--- /dev/null
+++ b/src/main/mse/sd/bash/commands/Command.java
@@ -0,0 +1,26 @@
+package mse.sd.bash.commands;
+
+import java.io.IOException;
+import java.io.Reader;
+
+public abstract class Command {
+ protected String[] args;
+ protected Command nextCommand = null;
+ protected String cwd;
+ public void setArgs(String[] args)
+ {
+ this.args = args;
+ }
+ public void setNext(Command command)
+ {
+ nextCommand = command;
+ }
+ public void setCWD(String cwd) {
+ this.cwd = cwd;
+ }
+ public abstract void eval(Reader reader) throws IOException;
+ public abstract void start() throws IOException;
+
+ public abstract Command getNew();
+
+}
diff --git a/src/main/mse/sd/bash/commands/Echo.java b/src/main/mse/sd/bash/commands/Echo.java
new file mode 100644
index 0000000..306cb32
--- /dev/null
+++ b/src/main/mse/sd/bash/commands/Echo.java
@@ -0,0 +1,31 @@
+package mse.sd.bash.commands;
+
+import java.io.*;
+import java.util.stream.Collectors;
+
+public class Echo extends Command {
+
+ @Override
+ public void eval(Reader reader) throws IOException {
+ if(nextCommand != null)
+ {
+ nextCommand.eval(reader);
+ }
+ else
+ {
+ System.out.println(new BufferedReader(reader).lines().collect(Collectors.joining("\n")));
+ }
+ }
+
+ @Override
+ public void start() throws IOException {
+ if(args.length > 0) {
+ eval(new StringReader(args[0]));
+ }
+ }
+
+ @Override
+ public Echo getNew() {
+ return new Echo();
+ }
+}
diff --git a/src/main/mse/sd/bash/commands/Exit.java b/src/main/mse/sd/bash/commands/Exit.java
new file mode 100644
index 0000000..58fd584
--- /dev/null
+++ b/src/main/mse/sd/bash/commands/Exit.java
@@ -0,0 +1,22 @@
+package mse.sd.bash.commands;
+
+import java.io.FileNotFoundException;
+import java.io.Reader;
+
+public class Exit extends Command {
+
+ @Override
+ public void eval(Reader reader) {
+ System.exit(0);
+ }
+
+ @Override
+ public void start() {
+ System.exit(0);
+ }
+
+ @Override
+ public Exit getNew() {
+ return new Exit();
+ }
+}
diff --git a/src/main/mse/sd/bash/commands/Ls.java b/src/main/mse/sd/bash/commands/Ls.java
new file mode 100644
index 0000000..86149d1
--- /dev/null
+++ b/src/main/mse/sd/bash/commands/Ls.java
@@ -0,0 +1,70 @@
+package mse.sd.bash.commands;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.File;
+import java.nio.file.*;
+
+public class Ls extends Command {
+ @Override
+ public void eval(Reader reader) throws IOException {
+ final int lineLengthLimit = 80;
+
+ BufferedReader br = new BufferedReader(reader);
+ Path p = Paths.get(br.readLine());
+ File f = p.toFile();
+ int lineLength = 0;
+ StringBuilder sb = new StringBuilder();
+ String result;
+ if (f.isDirectory()) {
+ for (File ff : f.listFiles()) {
+ String n = ff.getName();
+ if (lineLength + n.length() + 1 > lineLengthLimit) {
+ sb.append("\n");
+ lineLength = 0;
+ }
+ if (lineLength != 0) {
+ sb.append("\t");
+ ++lineLength;
+ }
+ sb.append(ff.getName());
+ lineLength += n.length();
+ }
+ result = sb.toString();
+ } else {
+ result = f.getName();
+ }
+ if(nextCommand != null)
+ {
+ nextCommand.eval(new StringReader(result));
+ }
+ else
+ {
+ System.out.println(result);
+ }
+ }
+
+ @Override
+ public void start() throws IOException {
+ if (args.length == 0) {
+ eval(new StringReader(cwd));
+ } else {
+ Path inputPath = Paths.get(args[0]);
+ if (!inputPath.isAbsolute()) {
+ inputPath = Paths.get(cwd).resolve(inputPath);
+ }
+ File f = inputPath.toFile();
+ if (!f.exists()) {
+ throw new IllegalArgumentException();
+ }
+ eval(new StringReader(inputPath.toString()));
+ }
+ }
+
+ @Override
+ public Command getNew() {
+ return new Ls();
+ }
+}
\ No newline at end of file
diff --git a/src/main/mse/sd/bash/commands/PipeManagerCommands.java b/src/main/mse/sd/bash/commands/PipeManagerCommands.java
new file mode 100644
index 0000000..0d315de
--- /dev/null
+++ b/src/main/mse/sd/bash/commands/PipeManagerCommands.java
@@ -0,0 +1,30 @@
+package mse.sd.bash.commands;
+
+import java.io.IOException;
+
+public class PipeManagerCommands {
+ private final Command[] commands;
+ public PipeManagerCommands(Command[] commands, String[][] args, String cwd)
+ {
+ this.commands = commands;
+ for (int i = 0;i < commands.length; i++)
+ {
+ commands[i].setArgs(args[i]);
+ }
+
+ for (int i = 0;i < commands.length - 1; i++)
+ {
+ commands[i].setNext(commands[i + 1]);
+ }
+
+ for (int i = 0;i < commands.length; i++)
+ {
+ commands[i].setCWD(cwd);
+ }
+ }
+
+ public void start() throws IOException {
+ commands[0].start();
+ }
+
+}
diff --git a/src/main/mse/sd/bash/commands/Pwd.java b/src/main/mse/sd/bash/commands/Pwd.java
new file mode 100644
index 0000000..2e3bd3e
--- /dev/null
+++ b/src/main/mse/sd/bash/commands/Pwd.java
@@ -0,0 +1,30 @@
+package mse.sd.bash.commands;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+
+public class Pwd extends Command {
+ @Override
+ public void eval(Reader reader) throws IOException {
+ String result = cwd;
+ if(nextCommand != null)
+ {
+ nextCommand.eval(new StringReader(result));
+ }
+ else
+ {
+ System.out.println(result);
+ }
+ }
+
+ @Override
+ public void start() throws IOException {
+ eval(new StringReader(""));
+ }
+
+ @Override
+ public Command getNew() {
+ return new Pwd();
+ }
+}
\ No newline at end of file
diff --git a/src/main/mse/sd/bash/commands/Wc.java b/src/main/mse/sd/bash/commands/Wc.java
new file mode 100644
index 0000000..165eb64
--- /dev/null
+++ b/src/main/mse/sd/bash/commands/Wc.java
@@ -0,0 +1,60 @@
+package mse.sd.bash.commands;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.StringTokenizer;
+import java.nio.file.*;
+
+public class Wc extends Command {
+
+ @Override
+ public void eval(Reader reader) {
+ StringBuilder stringBuilder = new StringBuilder();
+ try (BufferedReader bufferedReader = new BufferedReader(reader)) {
+ int lines = -1;
+ int words = 0;
+ int bytes = 0;
+
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ lines++;
+ bytes += line.getBytes(StandardCharsets.UTF_8).length; // проблемы
+ words += new StringTokenizer(line, " ").countTokens();
+ }
+ stringBuilder.append(String.format("%s %s %s %n", lines, words, bytes));
+ //System.out.printf("%s %s %s %s%n", lines, words, bytes, fileName);
+ //System.out.printf("%s %s %s %s%n", lines, words, bytes); ??
+ if(nextCommand != null)
+ {
+ nextCommand.eval(new StringReader(stringBuilder.toString()));
+ }
+ else
+ {
+ System.out.println(stringBuilder);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void start() {
+ try {
+ Path inputPath = Paths.get(args[0]);
+ if (!inputPath.isAbsolute()) {
+ inputPath = Paths.get(cwd).resolve(inputPath);
+ }
+ File f = inputPath.toFile();
+ if (!f.exists() || f.isDirectory()) {
+ throw new IllegalArgumentException();
+ }
+ eval(new FileReader(f));
+ } catch (Exception e) {
+ throw new IllegalArgumentException("error args");
+ }
+ }
+
+ @Override
+ public Command getNew() {
+ return new Wc();
+ }
+}
diff --git a/src/main/mse/sd/bash/commands/example.txt b/src/main/mse/sd/bash/commands/example.txt
new file mode 100644
index 0000000..9c4a1c4
--- /dev/null
+++ b/src/main/mse/sd/bash/commands/example.txt
@@ -0,0 +1,5 @@
+dsfsdfdsf sdfdsfds
+dsfsdfdsf sdfdsfds
+dsfsdfdsf sdfdsfds
+dsfsdfdsf sdfdsfds
+dsfsdfdsf sdfdsfds
diff --git a/src/test/CommandLineTest.java b/src/test/CommandLineTest.java
new file mode 100644
index 0000000..c7e7e6d
--- /dev/null
+++ b/src/test/CommandLineTest.java
@@ -0,0 +1,49 @@
+//package test;
+//
+//import static org.junit.Assert.assertEquals;
+//import org.junit.Test;
+//import main.mse.sd.bash.*;
+//public class CommandLineTest {
+//
+// @Test
+// public void testPwd() {
+// CommandLine commandLine = new CommandLine();
+// String expected = System.getProperty("user.dir");
+// String result = commandLine.executeCommand("pwd");
+// assertEquals(expected, result);
+// }
+//
+// @Test
+// public void testWc() {
+// CommandLine commandLine = new CommandLine();
+// String input = "This is a test string.";
+// int expected = input.split("\\s+").length;
+// String result = commandLine.executeCommand("wc", input);
+// assertEquals(Integer.toString(expected), result.trim());
+// }
+//
+// @Test
+// public void testCat() {
+// CommandLine commandLine = new CommandLine();
+// String[] lines = {"Line 1", "Line 2", "Line 3"};
+// String expected = String.join(System.lineSeparator(), lines);
+// String result = commandLine.executeCommand("cat", lines);
+// assertEquals(expected, result);
+// }
+//
+// @Test
+// public void testEcho() {
+// CommandLine commandLine = new CommandLine();
+// String message = "Hello, world!";
+// String result = commandLine.executeCommand("echo", message);
+// assertEquals(message, result);
+// }
+//
+// @Test
+// public void testExit() {
+// CommandLine commandLine = new CommandLine();
+// // Test that exit returns null (as the program should terminate)
+// String result = commandLine.executeCommand("exit");
+// assertEquals(null, result);
+// }
+//}
diff --git a/src/test/ParserTest.java b/src/test/ParserTest.java
new file mode 100644
index 0000000..08307de
--- /dev/null
+++ b/src/test/ParserTest.java
@@ -0,0 +1,4 @@
+//package test;
+//
+//public class ParserTest {
+//}
diff --git a/src/test/mse/sd/bash/CdTest.java b/src/test/mse/sd/bash/CdTest.java
new file mode 100644
index 0000000..d11e502
--- /dev/null
+++ b/src/test/mse/sd/bash/CdTest.java
@@ -0,0 +1,50 @@
+package mse.sd.bash;
+
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.*;
+
+import mse.sd.bash.commands.Cd;
+
+public class CdTest {
+ @Test
+ public void testCdRelative() {
+ final Cd cd = new Cd();
+ String cwd = System.getProperty("user.dir");
+ cd.setCWD(cwd);
+ cd.setArgs(new String[]{"./src"});
+ assertDoesNotThrow(() -> {
+ cd.start();
+ });
+ String expected = Paths.get(cwd, "./src").normalize().toString();
+ assertEquals(expected, cd.newCwd);
+ }
+
+ @Test
+ public void testCdAbsolute() {
+ String cwd = System.getProperty("user.dir");
+ String expected = Paths.get(cwd, "./src")
+ .toAbsolutePath()
+ .normalize()
+ .toString();
+ final Cd cd = new Cd();
+ cd.setCWD(cwd);
+ cd.setArgs(new String[]{expected});
+ assertDoesNotThrow(() -> {
+ cd.start();
+ });
+ assertEquals(expected, cd.newCwd);
+ }
+
+ @Test
+ public void testCdDoesNotExist() {
+ String cwd = System.getProperty("user.dir");
+ final Cd cd = new Cd();
+ cd.setCWD(cwd);
+ cd.setArgs(new String[]{"./doesnt_exist"});
+ assertThrows(IllegalArgumentException.class, () -> {
+ cd.start();
+ });
+ }
+}
diff --git a/src/test/mse/sd/bash/LsTest.java b/src/test/mse/sd/bash/LsTest.java
new file mode 100644
index 0000000..0111f1b
--- /dev/null
+++ b/src/test/mse/sd/bash/LsTest.java
@@ -0,0 +1,34 @@
+package mse.sd.bash;
+
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+import java.io.*;
+
+import mse.sd.bash.commands.Ls;
+
+public class LsTest {
+ @Test
+ public void testCdRelative() {
+ // Create a stream to hold the output
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ PrintStream ps = new PrintStream(baos);
+ // Save the old System.out
+ PrintStream old = System.out;
+ // Tell Java to use our special stream
+ System.setOut(ps);
+
+ final Ls ls = new Ls();
+ String cwd = System.getProperty("user.dir");
+ ls.setCWD(cwd);
+ ls.setArgs(new String[]{"./src"});
+ assertDoesNotThrow(() -> {
+ ls.start();
+ });
+ assertEquals("main\ttest", baos.toString().trim());
+
+ // Put things back
+ System.out.flush();
+ System.setOut(old);
+ }
+}