## Требования
- функциональные требования
- Ввод и исполнение команд
- `cat [FILE]` — вывести на экран содержимое файла;
- `echo` — вывести на экран свой аргумент (или аргументы);
- `wc [FILE] ` — вывести количество строк, слов и байт в файле;
- `pwd` — распечатать текущую директорию;
- `exit` — выйти из интерпретатора.
- одинарные и двойные кавычки (full and weak quoting);
- внутри одинарных кавычек значение строки берется как есть
- внутри двойных кавычек доступен слеш “\” и подставляются значения переменных
- “path is $PATH” -> `path is /etc/passwd`
- окружение (команды вида “имя=значение” для объявления переменных), оператор $ для подстановки значения переменной;
- вызов внешней программы, если введено что-то, чего интерпретатор не знает;
- пайплайны (оператор «|»), перенаправляющий вывод одной команды на вход другой
## Архитектурные требования
- легко добавлять новые команды;
- четкое разграничение ответственности между элементами архитектуры;
- это не должен быть просто клубок классов, требуется некая компонентная структура;
- наличие словесного архитектурного описания.
## Функциональные тесты
- Следующие команды выполнимы
```bash
> echo "Hello, world!"
Hello, world!
```
```bash
> FILE=example.txt
> cat $FILE
Some example text
```
```bash
> cat example.txt | wc
1 3 18
```
```bash
> echo 123 | wc
1 1 3
```
```bash
> x=ex
> y=it
> $x$y
```
```bash
> echo “path is $PATH”
path is /some/path
```
```bash
> echo ‘path is $PATH’
path is $PATH
```
```bash
> main.exe
Hello, World!
```
```bash
> cat file | exit
```
- Легко добавить новую команду
## Описание архитектуры
Главным классом приложения является класс `InterpretatorApp`, который содержит входную точку в приложение интерпретатора. Поле `environment` содержит все переменные, обьявленные пользователем. Работа класса `InterpretatorApp` может быть описана следующим псевдокодом:
```java
void main() {
while (true) {
line = input();
if (Parser.isAssignment(line)) {
this.set(Parser.parseAssignment(line));
} else {
cmd = Parser.parseLine(line);
res, exited = cmd.exec();
print(res);
if (exited)
break;
}
}
}
```
В процессе работы командная строка вводится пользователем в переменную `line`, интерпретатор парсит ее с помощью метода `parseLine` статического класса `Parser`, который возвращает обьект, позволяющий выполнить команду, и затем эта команда исполняется и ее вывод отображается пользователю. Если командная строка - присваивание переменной, то переменная и ее значение заносятся в `InterpreterApp.environment` с помощью метода `InterpreterApp.set`.
Статический класс `Parser` реализует функциональность разбора командной строки и конструирования команды по ней. Метод `parse` принимает командную строку и возвращает экземпляр `Executable`, выполняющий данную команду. Методы `isAssignment` и `parseAssignment` используются для проверки, что введеная строка обьявляет переменную, и выделения названия этой переменной и ее значения. Приватные методы класса `Parser` нужны для разбора строки и подстановки значений переменных. Метод `parseArbitaryQuotedString` принимает строку с произвольными кавычками и обрабатывает ее соответственно. Для работы с строкой с двойными кавычками используется метод `parseStringWithDoubleQuotes`.
В `cmd` записывается результат метода `Parser.parse`. Эта переменная хранит экземпляр интерфейса `Executable`, а значит является либо экземпляром `PipedCommand`, либо экземпляром одного из наследников абстрактного класса `AbstractCommand`. Далее вызывается метод `exec` этого обьекта, выполняющий соответствующую команду. Для поддержки пайплайнов класс `AbstractCommand` содержит метод `pipe`, который принимает вывод предыдущей команды и так же, как и `execute` возвращает результат текущей команды. Тогда метод `PipedCommand.exec` выглядит следующим образом:
```java
(String, bool) exec() {
out, exited = this.commands.exec();
for cmd in this.commands[1:] {
if (exited)
break;
out, exited = cmd.pipe(out);
}
}
```
Сами команды реализуются в виде наследников абстрактного класса `AbstractCommand` с помощью реализации методов `exec` и `pipe` для соответствующего поведения. Дефолтное поведение для команды определено в классе `AbstractCommand` подобным образом:
```java
(String, bool) exec() {
return ("", false);
}
(String, bool) pipe(input) {
return this.exec();
}
```
Таким образом, можно не переопределять метод `pipe`, так как будет использован переопределенный метод `exec`, что полезно, например, для команды `pwd`.
Команда `echo` будет выглядеть так:
```java
class Echo : AbstractCommand {
override (String, bool) exec() {
res = " ".join(this.arguments);
return (res, false);
}
override (String, bool) pipe(input) {
res = " ".join(this.arguments + [input]);
return (res, false);
}
}
```
Для добавления новой команды достаточно реализовать нового наследника класса `AbstractCommand` и добавить конструктор класса новой команды в класс `Parser`.
Приведенная архитектура проходит все описанные выше функциональные требования, позволяет легко добавлять новые команды и содержит модули с четко разделенной ответственностью.