# Haskell: ДЗ 3 -- Трансформеры монад и IO Данное домашнее задание нацелено на обучение навыкам использования стандартных монад, таких как `Reader`, `State`, `IO`, а также трансформеров монад. Умение использовать эти инструменты крайне необходимо для написания больших и полноценных приложений. В данном домашнем задании от Вас требуется реализовать интерпретатор уменьшенной версии `sh`. Каждое задание требует реализовать какую-то часть функциональности, в конце же Вам будет необходимо покрыть все тестами. При возникновении вопросов о семантике обращайтесь к документации `sh`, если не оговорено иное. ## Задание 1. Базовые конструкции Реализуйте базовые конструкции языка: * присвоение переменных * обращение к аргументам командной строки через `$0`, `$1`, `$2`... * одинарные кавычки (single quotes) Cкобки `(` и `)` считаются символами только находясь в кавычках. В любом другом случае, они являются управляющими конструкциями. Необходимо поддержать конструкцию `\$` и `\\` обозначающие символ `$` и `\` соответственно. `$` должен считаться символом, если он находится в одинарных кавычках, иначе он может использоваться только в конструкции`\$`. `\` считается символом находясь в одинарных кавычках, иначе он может использоваться только в конструкциях `\$` или `\\`. В любых других случаях должна выдаваться ошибка парсинга. Для парсинга Вы можете пользоваться библиотекой `megaparsec` Также в рамках данного задания, Вам требуется сделать запуск Вашего интерпрератора из командной строки. Так, например, если Ваш исполняемый файл называется `mySh` и Вы запускаете его с аргументами: `./mySh ~/script.sh 1 2 "aa aa"` он должен выполнить скрипт `~/script.sh`, с аргументами `1 2 "aa aa"`. Пример скрипта, который должен выполняться после выполнения этого задания, если Вы уберете из кода комментарии (такие примеры будут в каждом задании): ```bash a='aaa $1 bb' # a equals to 'aaa $1 bb' string b=$2 # b equals to the second passed argument a=b # a equals to 'b' c=$b # c equals to the second passed argument ``` ## Задание 2. Двойные кавычки Реализуйте поддержку двойных кавычек. Каждая подстрока, удовлетворяющая регулярному выражению: `$[a-zA-Z_0-9]+` считается ссылкой на переменную или аргумент. Переменную `$_` поддержитвать не нужно. Также не забудьте поддержать конструкцию `\"`. ```bash foo=mystring bla="My string: $1 and $2 -> $foo" # bla="My string: kek and bek -> mystring" # If `kek bek` was passed as an argument ``` ## Задание 3. Встроенные команды Поддержите в вашем интерпретаторе встроенные команды: * `read <varname1> <varname2> ...` - считывание строки * `echo <expr1> <expr2> ... ` - вывод на консоль * `pwd` - вывести на консоль путь к текущей рабочей директорию * `cd path` - изменить рабочую директорию * `exit` - выход из скрипта Также необходимо реализовать Команда `read` может принимать произвольное количество переменных. Если первыый аргумент `echo` `-n`, тогда `\n` печататься не должен. Команда `cd` принимает ровно один аргумент и ведет себя также как `cd` из `sh`. Если при запуске вашего исполняемого файла произошла какая-то синтаксическая ошибка в скрипте или выход с ненулевым кодом возврата - это должно сообщаться пользователю. ```bash echo 'Arguments:' $1 $2 $3 bla=34 echo 'bla:' $bla read i1 read i2 echo 'Read two lines:' "$i1 and $i2" read n m echo -n Read a line with a word $n echo " and" rest of line: $m cd .. pwd exit 0 echo "not reachable" ``` ## Задание 4. Внешние команды Реализуйте поддержку внешних команд `sh`. Внешние команды вызываются в формате `cmd arg1 arg2 ...`. Также необходимо реализовать поддержку встроенного вызова (inline call), который имеет вид `$(cmd1; cmd2;...)` Для большей информации о внешних командах можно прочитать [здесь](https://www.geeksforgeeks.org/internal-and-external-commands-in-linux/). Например, команда `pushd` не является внешней (хотя она и не является встроенной в наш язык). Вы можете использовать утилиту `type` в своем `shell`, чтобы узнать, является ли команда внешней или нет. ```bash cat $1 bla=$(ls) file=$1 foo="line count in $file: $(wc -l "$file")" echo "$bla ;; $foo ;; $non_existing" ``` Обратите внимание, что если в качестве аргумента скрипту выше передали `"foo bar"` (то есть два слова разделенных пробелом), `cat` будет вызвана с двумя аргументами. Чтобы передать `"foo bar"` как один аргумент, необходимо использовать синтаксис с двойными кавычками: `cat "$1"` ## Задание 5. Больше встроенных команд: if/while Реализуйте `if` and `while`. `if` в качестве условия принимает список команд, разделенных `;`, выполняет эти команды, и затем выполняет первую ветку кода, если команды завершились с нулевым кодом возврата, иначе вторую (если она есть). В условии `while` может находиться тоже самое, что и в `if`. Тело `while` продолжает выполняться до тех пор, пока команды команды завершаются с нулевым кодом возврата. ```bash while read l do echo "Line: $l" done if true; then echo "true" fi if false; then echo "won't happen" fi ``` Обратите внимание, что `true` и `false` это стандартные внешние команды в UNIX. Обратите внимание, что команды в условии `if` и `while` могут завершаться переводом строки. Для большего понимания обращайтесь к документации `sh`. ## Задание 6* На данный момент, Вы реализовали интерпретатор, который работает в монаде IO или в трансформере монад, где присутствует IO. Допустим у нас есть следующий скрипт: ```bash cd /usr rm bin/ls ``` Мы хотим протестировать его, но не хотели бы запускать его на собственной машине. Мы хотели бы иметь возможность запускать такие "in-memory", так чтобы они не производили изменений в нашей системе. В данном задании от Вас требуется написать "чистый" эмулятор Вашего интерпретатора. Эмулятор должен принимать структуру директорий (datatype описывающий ее, не привязанный к операционно системе), скрипт и данные которые являются входом с консоли, и запускать переданный скрипт. Эмулятор должен интерпретировать скрипт без какого либо `IO`, после чего возвращая структуру директорий после выполнения, а также данные, которые были выведены в консоль этим скриптом. После этого, Вам необходимо реализовать тесты используя этот эмулятор для вашего интерпретатора, которые должны находиться в папке `test/` и запускаться командной `stack test`. В этих тестах должны поддерживаться следующие команды: * встроенные команды * `rm`, `ls`, `cat` Обратите внимание, что в тестах можно не поддерживать полную семантику всех этих команд (например, не поддерживать флаги). **Примечание**: Чтобы реализовать это задание, Вам придется абстрагировать Ваш интерпретатор в `mtl` стиле и избавиться от конкретной монады в типе. Tо есть тип вашего интерпретатора должен стать каким-то таким: ```haskell interpret :: (MonadState Vars m, MonadConsole m) => m () ``` где `MonadConsole` - это ваш тайпкласс, который содержит операции работы с вводом/выводом в консоль. (Это всего лишь пример, на самом деле у Вас вероятно будет несколько больше констреинтов и они могут отличаться). Также Вам может могут понадобиться `ExceptT` [отсюда](http://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Except.html#t:ExceptT) или `CatchT` [отсюда](http://hackage.haskell.org/package/exceptions-0.10.1/docs/Control-Monad-Catch-Pure.html) (а также тайпклассы для них: `MonadError` и `MonadThrow`/`MonadCatch`), которые можно использовать в "чистом" эмуляторе. ## Бонус Помимо описанного вы можете реализовать поддержку pipe'ов вашим интерпретатором. Пример скрипта, который должен будет выполняться: ```bash ls | while read ln do echo ">> $ln" done | grep -v '.hs$' ``` За это задание не будут выставлены доп. баллы, но если за проверку других заданий будут минусы, до 3 минусованных баллов вам будет прощено. Мотивация реализовывать эту часть, однако, не в получении баллов, а just for fun, если вы также как один из авторов регулярно пользуютесь shell'ом и знаете что без пайпов shell -- штука бесполезная. ## Формат решений Ваше решение должно целиком содержаться в папке `hw3` корня репозитория. В папке должен быть `README.md` с перечислением заданий которые вы выполнили. Также Вы можете оставить комментарием для проверяющего в свободной форме, но это необязательно. Задание должно компилироваться с помощью `stack build --fast` и запускаться с помощью `stack exec -- mini-shell` из папки `hw3` (для чего нужно указать имя executable `mini-shell` в `.cabal` файле). К заданиям еще не были написаны тестовые скрипты. Во время исполнения задания Вам предлагается придумать набор таких тестов самостоятельно. Рекомендуется добавлять такие скрипты посредством pull request'а в https://github.com/georgeee/haskell-itmo-2019-hw3. Вы также можете использовать скрипты в этом репозитории для тестирования своих решения. Если в этом репозитории на момент проверки этого ДЗ для каждого задания будет достаточно тестов, проверяющие не будут утруждать себя сочинением новых. В обратном случае они приступят к этому после дедлайна и за решения, которые упадут на том или ином скрипте (соответствующем условиям) будут снижены баллы. ## Рекомендации по выполнению Пожалуйста, ознакомьтесь со слайдами лекций 6 и 7. Полезно вспомнить про рекомендации [со слайдов](https://slides.com/fp-ctd/lecture-7#/27) и описанные следом анти-паттерны. В частности, мы будем снижать баллы за комбинации `ExceptT IO` и `StateT IO`. Если вы используете `ReaderT Ctx IO`, пожалуйста оборачивайте в `IORef` только те части `Ctx`, которые могут быть изменены в ходе исполнения. Например: ```haskell data BadCtxImpl = BadCtxImpl { bciLog :: Text -> IO () , bciVars :: Map Int Text } type BadCtx = IORef BadCtxImpl data GoodCtx = GoodCtx { gcLog :: Text -> IO () , gcVars :: IORef (Map Int Text) } ```