diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e543976 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: python +python: + - "3.7-dev" +script: + - pytest diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..ff324c9 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,50 @@ +GREP + +Были рассмотрены ряд библиотек для парсинга аргументов: +* argparse +* optparse +* click +* docopt + +В результате был выбран argparse, по ряду причин: +* встроен в стандартную библиотеку и не требует установки +* нет проблем с лицензией +* большая популярность и множество примеров работы +* поддержка опциональных аргументов +* автогенерируемый help по аргументам и использованию команды + +Все остальные уступают в том или ином виде, в частности некоторые из них уже устарели (optparse) и считаются deprecated + + + +Интерпретатор командной строки, поддерживающий следующие команды: +* cat [FILE] — вывести на экран содержимое файла; +* echo — вывести на экран свой аргумент (или аргументы); +* wc [FILE] — вывести количество строк, слов и байт в файле; +* pwd — распечатать текущую директорию; +* exit — выйти из интерпретатора. + +Архитектура: + +Класс IToken - представляет собой интерфейс для токены, поиск которых будет осуществляться в исходном выражении, от него наследуются TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, +TokenAssignment, TokenWord, которые отвечают токенам в соответствии со своим названием. + +В последствии список классов токенов, которые необходимо распарсить передаются в класс, наследованный от интерфейса IParser, +который в свою очередь должен преобразовать входную строчку в набор токенов + +Класс IStorage представляет собой интерфейс для представления хранилища для переменных окружения, +его метод evaluate_variables, должен принимать строку и заменять вхождения всех переменных на их значения, +переменные ищутся по регулярному выражению, переданному в конструктор + +Команды и интерфейс ICommand представляют собой логику работы отдельной команды, из ключевых методов - +name, который должен вернуть имя команды, по которому будет выполнена подстановка и execute логика самой команды. Команды +реализованы согласно списку в начале файла. + +Интерпретатор ICommandInterpreter должен реализовывать преобразование потока токенов в поток команд с помощью +метода retrieve_commands, также при его создании необходимо указать разделитель команд (в нашем случае TokenPipe), а также +команду, которая будет выбрана по умолчанию, если все остальные команды не подошли + +IExecutor - аккумулирует все вышеперечисленное и должен преобразовывать входное выражение в результат его исполнения, что делает его +единственный метод execute_expression + +![alt text](./class_diagram.png) diff --git a/cli/class_diagram.png b/cli/class_diagram.png new file mode 100644 index 0000000..56f04ff Binary files /dev/null and b/cli/class_diagram.png differ diff --git a/cli/src/__init__.py b/cli/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/src/cli.py b/cli/src/cli.py new file mode 100644 index 0000000..9eb9a40 --- /dev/null +++ b/cli/src/cli.py @@ -0,0 +1,50 @@ +""" +интерпретатор командной строки, поддерживающий следующие команды: +• cat [FILE] — вывести на экран содержимое файла; +• echo — вывести на экран свой аргумент (или аргументы); +• wc [FILE] — вывести количество строк, слов и байт в файле; +• pwd — распечатать текущую директорию; +• exit — выйти из интерпретатора. +""" + +import os +from subprocess import run, PIPE +from src.storage import Storage +from src.commands import CommandCat, CommandEcho, CommandWC, CommandPwd, \ + CommandExit, CommandDefault, CommandGrep +from src.tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ + TokenAssignment, TokenWord +from src.interpreter import CommandInterpreterWithStorage +from src.pparser import Parser +from src.executor import Executor + + +def main_loop(): + """ + Главный цикл интерпретатора + """ + + if os.name == 'nt': # установка кодировки utf-8 для windows + run(['chcp', '65001'], stdout=PIPE, shell=True) + + storage = Storage(r'\$[^ \'\"$]+') + commands = [CommandCat, CommandEcho, CommandWC, + CommandPwd, CommandExit, CommandGrep] + token_types = [TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, + TokenAssignment, TokenWord] + + executor = Executor(CommandInterpreterWithStorage + (storage, commands, TokenPipe, CommandDefault), + Parser(token_types), storage) + + while True: + try: + result = executor.execute_expression(input("> ")) + if result: + print(result) + except Exception as error: + print(str(error)) + + +if __name__ == "__main__": + main_loop() diff --git a/cli/src/commands.py b/cli/src/commands.py new file mode 100644 index 0000000..a27c8e0 --- /dev/null +++ b/cli/src/commands.py @@ -0,0 +1,206 @@ +""" + Модуль с командами с которыми работает интерпретатор +""" +import os +import re +from abc import ABCMeta, abstractmethod +from typing import List +from subprocess import run, PIPE +from src.storage import IStorage +import argparse + + +class ICommand(metaclass=ABCMeta): + """ Интерфейс для команды """ + + def __init__(self, args: List[str]): + self._args = args + + @staticmethod + @abstractmethod + def name() -> str: + """ Вернуть имя команды + по нему будет происходить разбор команд""" + pass + + @abstractmethod + def execute(self, pipe: str, storage: IStorage) -> str: + """ Выполнить команду с переданным pipeом + вернуть результат выполнения """ + pass + + def prepend_arg(self, arg: str): + """ Добавить аргумент перед всеми аргументами """ + self._args = [arg] + self._args + + def append_arg(self, arg: str): + """ Добавить аргумент после всех аргументов """ + self._args.append(arg) + + +class CommandCat(ICommand): + """ Вывести на экран содержимое файла """ + + @staticmethod + def name() -> str: + return "cat" + + def execute(self, pipe: str, storage: IStorage) -> str: + if not self._args: + raise RuntimeError("cat: must specify file names!") + + result = "" + for filename in self._args: + try: + with open(filename) as f: + result += f.read() + '\n' + except IOError as error: + result += "cat: '%s' No such file or directory\n" % filename + + return result[:-1] + + +class CommandEcho(ICommand): + """ Вывести на экран свой аргумент (или аргументы) """ + + @staticmethod + def name() -> str: + return "echo" + + def execute(self, pipe: str, storage: IStorage) -> str: + return ' '.join(map(str, self._args)) + + +class CommandWC(ICommand): + """ Вывести количество строк, слов и байт в файле """ + + @staticmethod + def name() -> str: + return "wc" + + def execute(self, pipe: str, storage: IStorage) -> str: + result = "" + if pipe: + result += "%d %d %d\n" % (pipe.count('\n') + 1, + len(pipe.split()), + len(pipe)) + else: + if not self._args: + raise RuntimeError("wc: must specify file names!") + + for filename in self._args: + try: + with open(filename) as f: + data = f.read() + result += "%d %d %d\n" % (data.count('\n') + 1, + len(data.split()), + len(data)) + except IOError as error: + result += "wc: '%s' No such file or directory\n" % filename + + return result[:-1] + + +class CommandPwd(ICommand): + """ Распечатать текущую директорию """ + + @staticmethod + def name() -> str: + return "pwd" + + def execute(self, pipe: str, storage: IStorage) -> str: + return os.getcwd() + + +class CommandExit(ICommand): + """ Выйти из интерпретатора """ + + @staticmethod + def name() -> str: + return "exit" + + def execute(self, pipe: str, storage: IStorage) -> str: + quit(0) + return "" + + +class CommandDefault(ICommand): + """ Что будет выполнено, если не одна команда не подойдет """ + + @staticmethod + def name() -> str: + return "" + + def execute(self, pipe: str, storage: IStorage) -> str: + process = run(self._args, stdout=PIPE, input=pipe, + shell=True, encoding="utf8", errors='ignore') + return process.stdout + + +class CommandGrep(ICommand): + """ Команда grep, ищет паттерн в файле или во входном потоке""" + + @staticmethod + def name() -> str: + return "grep" + + @staticmethod + def read_from_file(files) -> (str, str): + pipe = "" + result = "" + for filename in files: + try: + with open(filename, encoding="utf8") as f: + pipe += f.read() + '\n' + except IOError as error: + result += "grep: '%s' No such file or directory\n" % filename + + return result, pipe + + def execute(self, pipe: str, storage: IStorage) -> str: + parser = argparse.ArgumentParser( + description='Search for PATTERN in each FILE') + + parser.add_argument("-i", "--ignore-case", action="store_true", + help="ignore case distinctions") + parser.add_argument("-w", "--word-regexp", action="store_true", + help="force PATTERN to match only whole words") + parser.add_argument("-A", "--after-context", type=int, + help="print NUM lines of output context") + parser.add_argument("PATTERN", + help="PATTERN is an extended regular expression") + parser.add_argument('FILE', nargs='*', + help='FILE is path to file for search') + + try: + args = parser.parse_args(self._args) + pattern = args.PATTERN + result = "" + + if args.after_context and args.after_context < 1: + raise argparse.ArgumentTypeError( + "lines NUM after context must be a positive integer") + + if args.word_regexp: + pattern = r"\b" + pattern + r"\b" + + if args.FILE: + res, pipe = self.read_from_file(args.FILE) + result += res + + after_lines_count = 0 + for line in pipe.splitlines(True): + if re.findall(pattern, line, + flags=re.IGNORECASE if args.ignore_case else 0): + after_lines_count = args.after_context if args.after_context else 0 + result += line + elif after_lines_count > 0: + result += line + after_lines_count -= 1 + + return result[:-1] + + except SystemExit: + pass + + return "" diff --git a/cli/src/executor.py b/cli/src/executor.py new file mode 100644 index 0000000..03ac12b --- /dev/null +++ b/cli/src/executor.py @@ -0,0 +1,37 @@ +""" + Модуль исполняющий команды интерпретатора +""" + +from abc import ABCMeta, abstractmethod +from src.interpreter import ICommandInterpreter +from src.pparser import IParser +from src.storage import IStorage + + +class IExecutor(metaclass=ABCMeta): + """ Интерфейс исполнителя выражений """ + + def __init__(self, command_interpreter: ICommandInterpreter, + parser: IParser, storage: IStorage): + self._command_interpreter = command_interpreter + self._parser = parser + self._storage = storage + + @abstractmethod + def execute_expression(self, expr: str) -> str: + """ Исполнить выражение и вернуть результат """ + pass + + +class Executor(IExecutor): + """ Реализация исполнителя выражений """ + + def execute_expression(self, expr: str) -> str: + tokens = self._parser.tokenize(expr) + commands = self._command_interpreter.retrieve_commands(iter(tokens)) + + result = "" + for command in commands: + result = command.execute(result, self._storage) + + return result diff --git a/cli/src/interpreter.py b/cli/src/interpreter.py new file mode 100644 index 0000000..87aea9e --- /dev/null +++ b/cli/src/interpreter.py @@ -0,0 +1,82 @@ +""" + Модуль интерпретатора команд +""" + +from abc import ABCMeta, abstractmethod +from typing import List, Type, Iterator, Generator +from src.tokens import IToken +from src.commands import ICommand +from src.storage import IStorage + + +class ICommandInterpreter(metaclass=ABCMeta): + """ Интерфейс парсера команд из потока токенов """ + + def __init__(self, command_types: List[Type[ICommand]], + delimiter: Type[IToken], default: Type[ICommand]): + self.__commands = dict() + # заполняем словарь комманд по их именам + for command in command_types: + if command != default: + self.__commands[command.name()] = command + + self.__delimiter = delimiter + self.__default = default + + @abstractmethod + def retrieve_commands(self, tokens: Iterator[IToken]) -> \ + Generator[ICommand, None, None]: + """ Преобразать список токенов в список команд """ + pass + + def default_command(self) -> Type[ICommand]: + """ Вернуть команду, которая будет выполнена, + если остальные команды не подойдут """ + return self.__default + + def delimiter(self) -> Type[IToken]: + """ Вернуть разделитель для команд внутри выражения """ + return self.__delimiter + + def retrieve_command(self, expr: str) -> Type[ICommand]: + """ Получить класс команды по имени команды """ + return self.__commands.get(expr, self.__default) + + +class CommandInterpreterWithStorage(ICommandInterpreter): + """ Реализация парсера команд из потока токенов """ + + def __init__(self, storage: IStorage, commands: List, + delimiter: Type[IToken], default: Type[ICommand]): + super().__init__(commands, delimiter, default) + self.__storage = storage + + def retrieve_commands(self, tokens: Iterator[IToken]) -> \ + Generator[ICommand, None, None]: + token = next(tokens, None) + + if not token: + return + + if not token.is_possibly_command(): + raise RuntimeError("Unexpected token: " + token.get_value()) + else: + token.eval_vars(self.__storage) + if token.execute(self.__storage): + args = [] + if self.retrieve_command(token.get_value()) \ + == self.default_command(): + args.append(token.get_value()) + + for arg_token in tokens: + if arg_token.__class__ != self.delimiter(): + arg_token.eval_vars(self.__storage) + args.append(arg_token.get_value()) + else: + yield self.retrieve_command(token.get_value())(args) + yield from self.retrieve_commands(tokens) + break + else: + yield self.retrieve_command(token.get_value())(args) + else: + yield from self.retrieve_commands(tokens) diff --git a/cli/src/pparser.py b/cli/src/pparser.py new file mode 100644 index 0000000..d220dfe --- /dev/null +++ b/cli/src/pparser.py @@ -0,0 +1,58 @@ +""" + Модуль парсера токенов +""" + +from abc import ABCMeta, abstractmethod +import itertools +import copy +import re +from typing import List, Type, Iterator +from src.tokens import IToken + + +class IParser(metaclass=ABCMeta): + """ Интерфейс парсера для токенов """ + + def __init__(self, token_types: List[Type[IToken]]): + # сортируем по приоритету + self._token_types = sorted(token_types, + key=lambda token_cls: token_cls.priority()) + + @abstractmethod + def tokenize(self, expression: str) -> List[IToken]: + """ Парсинг токенов из выражения """ + pass + + +class Parser(IParser): + """ Реализация парсера токенов """ + + def tokenize(self, expression: str) -> List[IToken]: + def tokenize_with_types(expr: str, token_types: Iterator[Type[IToken]]) \ + -> List[IToken]: + """ Рекурсивно парсим выражение по приориету токенов, + каждый токен разбивает выражение на два подвыражения и т.д. """ + tokens = [] + token_class = next(token_types, None) + if expr and token_class: + tokens_of_type = re.findall(token_class.regexp(), expr) + + # удаляем группы из регулярного выражения, чтобы не было + # лишней информации в подвыражениях + split_by = re.sub(r'(? bool: + """ Проверка наличия переменной в окружении """ + pass + + @abstractmethod + def __setitem__(self, key: str, value: str) -> None: + """ Добавить или обновить переменную окружения """ + pass + + @abstractmethod + def __getitem__(self, key: str): + """ Запросить значение переменной окружения """ + pass + + @abstractmethod + def evaluate_variables(self, expression: str) -> str: + """ Заменить переменные окружения на их значения из хранилища + возвращает новую строку с проведенной заменой """ + pass + + +class Storage(IStorage): + """ Реализация интерфейса для хранения и доступа к переменным окружения """ + + def __init__(self, regexp: str): + self.__storage = dict() + self.__regexp = regexp + + def __contains__(self, key: str) -> bool: + return key in self.__storage + + def __setitem__(self, key: str, value: str) -> None: + self.__storage[key] = value + + def __getitem__(self, key: str) -> str: + return self.__storage[key] + + def evaluate_variables(self, expression: str) -> str: + return re.sub(self.__regexp, + lambda var: self.__storage.get(var.group()[1:], ""), + expression) diff --git a/cli/src/tokens.py b/cli/src/tokens.py new file mode 100644 index 0000000..6f9aeb7 --- /dev/null +++ b/cli/src/tokens.py @@ -0,0 +1,204 @@ +""" + Модуль с токенамами с которыми работает интерпретатор +""" + +from abc import ABCMeta, abstractmethod +from src.storage import IStorage + + +class IToken(metaclass=ABCMeta): + """ Интерфейс для токена, поиск которого будет выполняться при обработке + входных данных интерпретатора """ + + @abstractmethod + def __init__(self, regexp_result): + pass + + @staticmethod + @abstractmethod + def priority() -> int: + """ Приоритет поиска токена во входном выражении + должен вернуть число от 0 до inf """ + pass + + @staticmethod + @abstractmethod + def regexp() -> str: + """ Регулярное выражение для поиска токена + результат поиска вернется в метод __init__ """ + pass + + @abstractmethod + def set_value(self, value: str) -> None: + """ Установка значения токена """ + pass + + @abstractmethod + def get_value(self) -> str: + """ Получение значения токена """ + pass + + @abstractmethod + def eval_vars(self, storage: IStorage) -> None: + """ Замена переменных окружения на их значения, + если это необходимо """ + pass + + @abstractmethod + def is_possibly_command(self) -> bool: + """ Возможность токена содержать в себе команду """ + pass + + def execute(self, storage: IStorage) -> bool: + """ Код, который должен быть исполнен, + если токен станет командой, + возвращает true если его нужно исполнить как команду """ + return True + + +class TokenInSingleQuotes(IToken): + """ Токен для работы с одинарными кавычками в выражении """ + + def __init__(self, regexp_result): + self.__value = regexp_result if regexp_result else "" + + @staticmethod + def priority() -> int: + return 16 + + @staticmethod + def regexp() -> str: + return '[\']([^\']*)[\']' + + def set_value(self, value: str) -> None: + self.__value = value + + def get_value(self) -> str: + return self.__value + + def eval_vars(self, storage: IStorage) -> None: + pass + + def is_possibly_command(self) -> bool: + return True + + +class TokenInDoubleQuotes(IToken): + """ Токен для двойных кавычек в выражении """ + + def __init__(self, regexp_result): + self.__value = regexp_result if regexp_result else "" + + @staticmethod + def priority() -> int: + return 32 + + @staticmethod + def regexp() -> str: + return '[\"]([^\"]*)[\"]' + + def set_value(self, value: str) -> None: + self.__value = value + + def get_value(self) -> str: + return self.__value + + def eval_vars(self, storage: IStorage) -> None: + self.set_value(storage.evaluate_variables(self.get_value())) + + def is_possibly_command(self) -> bool: + return True + + +class TokenPipe(IToken): + """ Токен для пайпа в выражении""" + + def __init__(self, regexp_result): + pass + + @staticmethod + def priority() -> int: + return 48 + + @staticmethod + def regexp() -> str: + return '\\|' + + def set_value(self, value: str) -> None: + pass + + def get_value(self) -> str: + return '|' + + def eval_vars(self, storage: IStorage) -> None: + pass + + def is_possibly_command(self) -> bool: + return False + + +class TokenAssignment(IToken): + """ Токен для работы с заданием переменных окружения в выражении """ + + def __init__(self, regexp_result): + self.__var = regexp_result[0] if regexp_result else "" + self.__val = regexp_result[1] if regexp_result else "" + + @staticmethod + def priority() -> int: + return 64 + + @staticmethod + def regexp() -> str: + return '([^ =]+)=([^ ]*)' + + def set_value(self, value: str) -> None: + buffer = value.split('=') + if len(buffer) == 2: + self.__var = buffer[0] + self.__val = buffer[1] + else: + raise RuntimeError("Invalid value for TokenAssignment: " + value) + + def get_value(self) -> str: + return self.__var + '=' + self.__val + + def eval_vars(self, storage: IStorage) -> None: + self.__val = storage.evaluate_variables(self.__val) + self.__var = storage.evaluate_variables(self.__var) + + def is_possibly_command(self) -> bool: + return True + + def execute(self, storage: IStorage) -> bool: + """ Сохранить переменную в окружении """ + storage[self.__var] = self.__val + return False + + +class TokenWord(IToken): + """ Токен для парсинга прочих ключевых слов, которые не подошли под + остальные токены """ + + def __init__(self, regexp_result): + self.__value = regexp_result if regexp_result else "" + + @staticmethod + def priority() -> int: + return 128 + + @staticmethod + def regexp() -> str: + return '[^ \'\"|]+' + + def set_value(self, value: str) -> None: + self.__value = value + + def get_value(self) -> str: + return self.__value + + def eval_vars(self, storage: IStorage) -> None: + self.set_value(storage.evaluate_variables(self.get_value())) + + def is_possibly_command(self) -> bool: + return True diff --git a/cli/tests/__init__.py b/cli/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/tests/example.txt b/cli/tests/example.txt new file mode 100644 index 0000000..65be4ed --- /dev/null +++ b/cli/tests/example.txt @@ -0,0 +1 @@ +Some example text \ No newline at end of file diff --git a/cli/tests/grep_test b/cli/tests/grep_test new file mode 100644 index 0000000..899ee49 --- /dev/null +++ b/cli/tests/grep_test @@ -0,0 +1,4 @@ +apply plugin: 'java' +apply plugin: 'idea' +group = 'ru.example' +version = '1.0' \ No newline at end of file diff --git a/cli/tests/test_command_cat.py b/cli/tests/test_command_cat.py new file mode 100644 index 0000000..828b81c --- /dev/null +++ b/cli/tests/test_command_cat.py @@ -0,0 +1,17 @@ +import os +from unittest import TestCase + +from src.commands import CommandCat +from src.storage import Storage + + +class TestCommandCat(TestCase): + def test_execute(self): + storage = Storage(r'\$[^ \'\"$]+') + + command = CommandCat([os.path.dirname(__file__) + '/example.txt']) + self.assertEqual(command.execute("", storage), "Some example text") + + command = CommandCat(['dsakfjhakdsljf']) + self.assertEqual(command.execute("", storage), "cat: 'dsakfjhakdsljf' " + "No such file or directory") diff --git a/cli/tests/test_command_echo.py b/cli/tests/test_command_echo.py new file mode 100644 index 0000000..ed9e3af --- /dev/null +++ b/cli/tests/test_command_echo.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from src.commands import CommandEcho +from src.storage import Storage + + +class TestCommandEcho(TestCase): + def test_execute(self): + storage = Storage(r'\$[^ \'\"$]+') + + command = CommandEcho(['123', 'asd']) + self.assertEqual(command.execute("", storage), "123 asd") diff --git a/cli/tests/test_command_grep.py b/cli/tests/test_command_grep.py new file mode 100644 index 0000000..9d847e0 --- /dev/null +++ b/cli/tests/test_command_grep.py @@ -0,0 +1,33 @@ +import os +from unittest import TestCase + +from src.commands import CommandGrep +from src.storage import Storage + + +class TestCommandGrep(TestCase): + def test_execute(self): + storage = Storage(r'\$[^ \'\"$]+') + + command = CommandGrep(['-i', '-w', '-A 2', 'plugin', + os.path.dirname(__file__) + '/grep_test']) + self.assertEqual(command.execute("", storage), "apply plugin: 'java'\n" + "apply plugin: 'idea'\n" + "group = 'ru.example'\n" + "version = '1.0'") + + command = CommandGrep(['-i', '-w', '-A 2', 'plugi', + os.path.dirname(__file__) + '/grep_test']) + self.assertEqual(command.execute("", storage), "") + + command = CommandGrep(['-i', '-w', '-A 2', 'PluGin', + os.path.dirname(__file__) + '/grep_test']) + self.assertEqual(command.execute("", storage), "apply plugin: 'java'\n" + "apply plugin: 'idea'\n" + "group = 'ru.example'\n" + "version = '1.0'") + + command = CommandGrep( + ['plugin', os.path.dirname(__file__) + '/grep_test']) + self.assertEqual(command.execute("", storage), "apply plugin: 'java'\n" + "apply plugin: 'idea'") diff --git a/cli/tests/test_command_interpreterpy.py b/cli/tests/test_command_interpreterpy.py new file mode 100644 index 0000000..e3b87e1 --- /dev/null +++ b/cli/tests/test_command_interpreterpy.py @@ -0,0 +1,31 @@ +from unittest import TestCase + +from src.commands import CommandCat, CommandExit, CommandEcho, CommandWC, \ + CommandPwd, CommandDefault +from src.interpreter import CommandInterpreterWithStorage +from src.pparser import Parser +from src.storage import Storage +from src.tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ + TokenAssignment, TokenWord + + +class TestCommandInterpreterWithStorage(TestCase): + def test_retrieve_commands(self): + storage = Storage(r'\$[^ \'\"$]+') + commands = [CommandCat, CommandEcho, CommandWC, + CommandPwd, CommandExit] + parser = Parser([TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, + TokenAssignment, TokenWord]) + interpreter = CommandInterpreterWithStorage(storage, commands, + TokenPipe, CommandDefault) + + tokens = parser.tokenize("echo 123 | exit | wc | cat | pwd | sdfxvc") + commands = list(interpreter.retrieve_commands(iter(tokens))) + + self.assertEqual(len(commands), 6) + self.assertEqual(commands[0].__class__, CommandEcho) + self.assertEqual(commands[1].__class__, CommandExit) + self.assertEqual(commands[2].__class__, CommandWC) + self.assertEqual(commands[3].__class__, CommandCat) + self.assertEqual(commands[4].__class__, CommandPwd) + self.assertEqual(commands[5].__class__, CommandDefault) diff --git a/cli/tests/test_command_wc.py b/cli/tests/test_command_wc.py new file mode 100644 index 0000000..d32c949 --- /dev/null +++ b/cli/tests/test_command_wc.py @@ -0,0 +1,21 @@ +import os +from unittest import TestCase + +from src.commands import CommandWC +from src.storage import Storage + + +class TestCommandWC(TestCase): + def test_execute(self): + storage = Storage(r'\$[^ \'\"$]+') + + command = CommandWC([os.path.dirname(__file__) + '/example.txt']) + self.assertEqual(command.execute("", storage), "1 3 17") + + command = CommandWC([]) + self.assertEqual(command.execute('Some example text', storage), + "1 3 17") + + command = CommandWC(['sdfsdfsdf']) + self.assertEqual(command.execute('', storage), "wc: 'sdfsdfsdf'" + " No such file or directory") diff --git a/cli/tests/test_executor.py b/cli/tests/test_executor.py new file mode 100644 index 0000000..6c52da0 --- /dev/null +++ b/cli/tests/test_executor.py @@ -0,0 +1,36 @@ +import os +from unittest import TestCase + +from src.storage import Storage +from src.commands import CommandCat, CommandEcho, CommandWC, CommandPwd, \ + CommandExit, CommandDefault +from src.tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ + TokenAssignment, TokenWord +from src.interpreter import CommandInterpreterWithStorage +from src.pparser import Parser +from src.executor import Executor + + +class TestExecutor(TestCase): + def test_execute_expression(self): + storage = Storage(r'\$[^ \'\"$]+') + commands = [CommandCat, CommandEcho, CommandWC, + CommandPwd, CommandExit] + token_types = [TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, + TokenAssignment, TokenWord] + + executor = Executor(CommandInterpreterWithStorage + (storage, commands, TokenPipe, CommandDefault), + Parser(token_types), storage) + + self.assertEqual(executor.execute_expression('echo "Hello, world!"'), + 'Hello, world!') + + self.assertEqual(executor.execute_expression( + 'FILE=' + os.path.dirname(__file__) + '/example.txt'), '') + + self.assertEqual(executor.execute_expression('cat $FILE'), + 'Some example text') + + self.assertEqual(executor.execute_expression('cat $FILE | wc'), + '1 3 17') diff --git a/cli/tests/test_parser.py b/cli/tests/test_parser.py new file mode 100644 index 0000000..cd3e4d1 --- /dev/null +++ b/cli/tests/test_parser.py @@ -0,0 +1,16 @@ +from unittest import TestCase + +from src.pparser import Parser +from src.tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ + TokenAssignment, TokenWord + + +class TestParser(TestCase): + def test_tokenize(self): + parser = Parser([TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, + TokenAssignment, TokenWord]) + + self.assertEqual(len(parser.tokenize("echo 123 | wc")), 4) + self.assertEqual(len(parser.tokenize("echo \"13 xc kdf akf\" | wc")), 4) + self.assertEqual(len(parser.tokenize("echo '13 xc kdf akf' | wc")), 4) + self.assertEqual(len(parser.tokenize("xcv=3234 nnn=123")), 2) diff --git a/cli/tests/test_storage.py b/cli/tests/test_storage.py new file mode 100644 index 0000000..cd1e424 --- /dev/null +++ b/cli/tests/test_storage.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from src.storage import Storage + + +class TestStorage(TestCase): + def test_evaluate_variables(self): + storage = Storage(r'\$[^ \'\"$]+') + storage['a'] = '123' + self.assertEqual(storage.evaluate_variables("qwe$a"), "qwe123") + self.assertEqual(storage.evaluate_variables("$a$a$a"), "123123123") + self.assertEqual(storage.evaluate_variables("aaa aaa"), "aaa aaa")