From 3d201a399d43564f48b9486cae49361adf125d8c Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Sun, 24 Feb 2019 16:21:14 +0300 Subject: [PATCH 01/15] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BC=D0=B0=D1=88=D0=BD=D1=8F=D1=8F=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/README.md | 29 ++++ cli/cli.py | 50 ++++++ cli/commands.py | 134 ++++++++++++++++ cli/example.txt | 1 + cli/executor.py | 35 ++++ cli/interpreter.py | 82 ++++++++++ cli/pparser.py | 59 +++++++ cli/storage.py | 58 +++++++ cli/tests/test_command_cat.py | 13 ++ cli/tests/test_command_echo.py | 9 ++ cli/tests/test_command_interpreterpy.py | 31 ++++ cli/tests/test_command_wc.py | 16 ++ cli/tests/test_executor.py | 34 ++++ cli/tests/test_parser.py | 16 ++ cli/tests/test_storage.py | 12 ++ cli/tokens.py | 204 ++++++++++++++++++++++++ 16 files changed, 783 insertions(+) create mode 100644 cli/README.md create mode 100644 cli/cli.py create mode 100644 cli/commands.py create mode 100644 cli/example.txt create mode 100644 cli/executor.py create mode 100644 cli/interpreter.py create mode 100644 cli/pparser.py create mode 100644 cli/storage.py create mode 100644 cli/tests/test_command_cat.py create mode 100644 cli/tests/test_command_echo.py create mode 100644 cli/tests/test_command_interpreterpy.py create mode 100644 cli/tests/test_command_wc.py create mode 100644 cli/tests/test_executor.py create mode 100644 cli/tests/test_parser.py create mode 100644 cli/tests/test_storage.py create mode 100644 cli/tokens.py diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..e2655c3 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,29 @@ +Интерпретатор командной строки, поддерживающий следующие команды: +* 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 \ No newline at end of file diff --git a/cli/cli.py b/cli/cli.py new file mode 100644 index 0000000..5719860 --- /dev/null +++ b/cli/cli.py @@ -0,0 +1,50 @@ +""" +интерпретатор командной строки, поддерживающий следующие команды: +• cat [FILE] — вывести на экран содержимое файла; +• echo — вывести на экран свой аргумент (или аргументы); +• wc [FILE] — вывести количество строк, слов и байт в файле; +• pwd — распечатать текущую директорию; +• exit — выйти из интерпретатора. +""" + +import os +from subprocess import run, PIPE +from storage import Storage +from commands import CommandCat, CommandEcho, CommandWC, CommandPwd, \ + CommandExit, CommandDefault +from tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ + TokenAssignment, TokenWord +from interpreter import CommandInterpreterWithStorage +from pparser import Parser +from 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] + token_types = [TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, + TokenAssignment, TokenWord] + + executor = Executor(CommandInterpreterWithStorage + (storage, commands, TokenPipe, CommandDefault), + Parser(token_types)) + + 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/commands.py b/cli/commands.py new file mode 100644 index 0000000..b0bdbc4 --- /dev/null +++ b/cli/commands.py @@ -0,0 +1,134 @@ +""" + Модуль с командами с которыми работает интерпретатор +""" + +from abc import ABCMeta, abstractmethod +from typing import List +from subprocess import run, PIPE + + +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) -> 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) -> 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) -> str: + return ' '.join(map(str, self._args)) + + +class CommandWC(ICommand): + """ Вывести количество строк, слов и байт в файле """ + + @staticmethod + def name() -> str: + return "wc" + + def execute(self, pipe: str) -> 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) -> str: + return os.getcwd() + + +class CommandExit(ICommand): + """ Выйти из интерпретатора """ + + @staticmethod + def name() -> str: + return "exit" + + def execute(self, pipe: str) -> str: + quit(0) + return "" + + +class CommandDefault(ICommand): + """ Что будет выполнено, если не одна команда не подойдет """ + + @staticmethod + def name() -> str: + return "" + + def execute(self, pipe: str) -> str: + process = run(self._args, stdout=PIPE, input=pipe, + shell=True, encoding="utf8", errors='ignore') + return process.stdout diff --git a/cli/example.txt b/cli/example.txt new file mode 100644 index 0000000..65be4ed --- /dev/null +++ b/cli/example.txt @@ -0,0 +1 @@ +Some example text \ No newline at end of file diff --git a/cli/executor.py b/cli/executor.py new file mode 100644 index 0000000..b2a95a9 --- /dev/null +++ b/cli/executor.py @@ -0,0 +1,35 @@ +""" + Модуль исполняющий команды интерпретатора +""" + +from abc import ABCMeta, abstractmethod +from interpreter import ICommandInterpreter +from pparser import IParser + + +class IExecutor(metaclass=ABCMeta): + """ Интерфейс исполнителя выражений """ + + def __init__(self, command_interpreter: ICommandInterpreter, + parser: IParser): + self._command_interpreter = command_interpreter + self._parser = parser + + @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) + + return result diff --git a/cli/interpreter.py b/cli/interpreter.py new file mode 100644 index 0000000..73afc59 --- /dev/null +++ b/cli/interpreter.py @@ -0,0 +1,82 @@ +""" + Модуль интерпретатора команд +""" + +from abc import ABCMeta, abstractmethod +from typing import List, Type, Iterator, Generator +from tokens import IToken +from commands import ICommand +from 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: + raise StopIteration() + + 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/pparser.py b/cli/pparser.py new file mode 100644 index 0000000..f2e7d6c --- /dev/null +++ b/cli/pparser.py @@ -0,0 +1,59 @@ +""" + Модуль парсера токенов +""" + +from abc import ABCMeta, abstractmethod +import itertools +import copy +import re +from typing import List, Type, Iterator +from 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/tests/test_command_cat.py b/cli/tests/test_command_cat.py new file mode 100644 index 0000000..99e492a --- /dev/null +++ b/cli/tests/test_command_cat.py @@ -0,0 +1,13 @@ +from unittest import TestCase + +from commands import CommandCat + + +class TestCommandCat(TestCase): + def test_execute(self): + command = CommandCat(['../example.txt']) + self.assertEqual(command.execute(""), "Some example text") + + command = CommandCat(['dsakfjhakdsljf']) + self.assertEqual(command.execute(""), "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..b00f95d --- /dev/null +++ b/cli/tests/test_command_echo.py @@ -0,0 +1,9 @@ +from unittest import TestCase + +from commands import CommandEcho + + +class TestCommandEcho(TestCase): + def test_execute(self): + command = CommandEcho(['123', 'asd']) + self.assertEqual(command.execute(""), "123 asd") diff --git a/cli/tests/test_command_interpreterpy.py b/cli/tests/test_command_interpreterpy.py new file mode 100644 index 0000000..74ebd78 --- /dev/null +++ b/cli/tests/test_command_interpreterpy.py @@ -0,0 +1,31 @@ +from unittest import TestCase + +from commands import CommandCat, CommandExit, CommandEcho, CommandWC, \ + CommandPwd, CommandDefault +from interpreter import CommandInterpreterWithStorage +from pparser import Parser +from storage import Storage +from 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..185e22c --- /dev/null +++ b/cli/tests/test_command_wc.py @@ -0,0 +1,16 @@ +from unittest import TestCase + +from commands import CommandWC + + +class TestCommandWC(TestCase): + def test_execute(self): + command = CommandWC(['../example.txt']) + self.assertEqual(command.execute(""), "1 3 17") + + command = CommandWC([]) + self.assertEqual(command.execute('Some example text'), "1 3 17") + + command = CommandWC(['sdfsdfsdf']) + self.assertEqual(command.execute(''), "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..eeb15ab --- /dev/null +++ b/cli/tests/test_executor.py @@ -0,0 +1,34 @@ +from unittest import TestCase + +from storage import Storage +from commands import CommandCat, CommandEcho, CommandWC, CommandPwd, \ + CommandExit, CommandDefault +from tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ + TokenAssignment, TokenWord +from interpreter import CommandInterpreterWithStorage +from pparser import Parser +from 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)) + + self.assertEqual(executor.execute_expression('echo "Hello, world!"'), + 'Hello, world!') + + self.assertEqual(executor.execute_expression('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..a22f8a2 --- /dev/null +++ b/cli/tests/test_parser.py @@ -0,0 +1,16 @@ +from unittest import TestCase + +from pparser import Parser +from 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..cf1109d --- /dev/null +++ b/cli/tests/test_storage.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from 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") diff --git a/cli/tokens.py b/cli/tokens.py new file mode 100644 index 0000000..2f2529a --- /dev/null +++ b/cli/tokens.py @@ -0,0 +1,204 @@ +""" + Модуль с токенамами с которыми работает интерпретатор +""" + +from abc import ABCMeta, abstractmethod +from 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 From 0a2e8ed0b286619e6559c65c261d3dde529285f6 Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 13:06:17 +0300 Subject: [PATCH 02/15] =?UTF-8?q?*=20=D0=B4=D0=B8=D0=B0=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D0=BC=D0=BC=D0=B0=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D0=BE=D0=B2?= =?UTF-8?q?=20*=20=D0=B2=20execute=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B0=D1=82=D1=8C=20IStorage,=20=D1=80=D0=B5=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B4=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BD=D0=B5=D0=B3=D0=BE=20=D1=85?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B8=D1=82=D1=8C=20=D0=BE=D0=BA=D1=80=D1=83?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/README.md | 4 +++- cli/class_diagram.png | Bin 0 -> 82395 bytes cli/cli.py | 2 +- cli/commands.py | 15 ++++++++------- cli/executor.py | 6 ++++-- cli/pparser.py | 39 +++++++++++++++++++-------------------- 6 files changed, 35 insertions(+), 31 deletions(-) create mode 100644 cli/class_diagram.png diff --git a/cli/README.md b/cli/README.md index e2655c3..f3c3a0b 100644 --- a/cli/README.md +++ b/cli/README.md @@ -26,4 +26,6 @@ name, который должен вернуть имя команды, по к команду, которая будет выбрана по умолчанию, если все остальные команды не подошли IExecutor - аккумулирует все вышеперечисленное и должен преобразовывать входное выражение в результат его исполнения, что делает его -единственный метод execute_expression \ No newline at end of file +единственный метод execute_expression + +![alt text](./class_diagram.png) \ No newline at end of file diff --git a/cli/class_diagram.png b/cli/class_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..56f04ffd75ed7503a4ce64bfbd614a1055bb6822 GIT binary patch literal 82395 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 literal 0 HcmV?d00001 diff --git a/cli/cli.py b/cli/cli.py index 5719860..1ef06ab 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -35,7 +35,7 @@ def main_loop(): executor = Executor(CommandInterpreterWithStorage (storage, commands, TokenPipe, CommandDefault), - Parser(token_types)) + Parser(token_types), storage) while True: try: diff --git a/cli/commands.py b/cli/commands.py index b0bdbc4..8d8290e 100644 --- a/cli/commands.py +++ b/cli/commands.py @@ -5,6 +5,7 @@ from abc import ABCMeta, abstractmethod from typing import List from subprocess import run, PIPE +from storage import IStorage class ICommand(metaclass=ABCMeta): @@ -21,7 +22,7 @@ def name() -> str: pass @abstractmethod - def execute(self, pipe: str) -> str: + def execute(self, pipe: str, storage: IStorage) -> str: """ Выполнить команду с переданным pipeом вернуть результат выполнения """ pass @@ -42,7 +43,7 @@ class CommandCat(ICommand): def name() -> str: return "cat" - def execute(self, pipe: str) -> str: + def execute(self, pipe: str, storage: IStorage) -> str: if not self._args: raise RuntimeError("cat: must specify file names!") @@ -64,7 +65,7 @@ class CommandEcho(ICommand): def name() -> str: return "echo" - def execute(self, pipe: str) -> str: + def execute(self, pipe: str, storage: IStorage) -> str: return ' '.join(map(str, self._args)) @@ -75,7 +76,7 @@ class CommandWC(ICommand): def name() -> str: return "wc" - def execute(self, pipe: str) -> str: + def execute(self, pipe: str, storage: IStorage) -> str: result = "" if pipe: result += "%d %d %d\n" % (pipe.count('\n') + 1, @@ -105,7 +106,7 @@ class CommandPwd(ICommand): def name() -> str: return "pwd" - def execute(self, pipe: str) -> str: + def execute(self, pipe: str, storage: IStorage) -> str: return os.getcwd() @@ -116,7 +117,7 @@ class CommandExit(ICommand): def name() -> str: return "exit" - def execute(self, pipe: str) -> str: + def execute(self, pipe: str, storage: IStorage) -> str: quit(0) return "" @@ -128,7 +129,7 @@ class CommandDefault(ICommand): def name() -> str: return "" - def execute(self, pipe: str) -> str: + 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 diff --git a/cli/executor.py b/cli/executor.py index b2a95a9..973debf 100644 --- a/cli/executor.py +++ b/cli/executor.py @@ -5,15 +5,17 @@ from abc import ABCMeta, abstractmethod from interpreter import ICommandInterpreter from pparser import IParser +from storage import IStorage class IExecutor(metaclass=ABCMeta): """ Интерфейс исполнителя выражений """ def __init__(self, command_interpreter: ICommandInterpreter, - parser: IParser): + parser: IParser, storage: IStorage): self._command_interpreter = command_interpreter self._parser = parser + self._storage = storage @abstractmethod def execute_expression(self, expr: str) -> str: @@ -30,6 +32,6 @@ def execute_expression(self, expr: str) -> str: result = "" for command in commands: - result = command.execute(result) + result = command.execute(result, self._storage) return result diff --git a/cli/pparser.py b/cli/pparser.py index f2e7d6c..4bcfea4 100644 --- a/cli/pparser.py +++ b/cli/pparser.py @@ -33,26 +33,25 @@ def tokenize_with_types(expr: str, token_types: Iterator[Type[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'(? Date: Fri, 1 Mar 2019 15:09:02 +0300 Subject: [PATCH 03/15] =?UTF-8?q?=D0=94=D0=BE=D0=BC=D0=B0=D1=88=D0=BD?= =?UTF-8?q?=D1=8F=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/README.md | 21 +++++++++++- cli/cli.py | 4 +-- cli/commands.py | 61 +++++++++++++++++++++++++++++++++- cli/grep_test | 4 +++ cli/interpreter.py | 2 +- cli/tests/test_command_cat.py | 9 +++-- cli/tests/test_command_echo.py | 5 ++- cli/tests/test_command_grep.py | 28 ++++++++++++++++ cli/tests/test_command_wc.py | 12 ++++--- cli/tests/test_executor.py | 2 +- 10 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 cli/grep_test create mode 100644 cli/tests/test_command_grep.py diff --git a/cli/README.md b/cli/README.md index f3c3a0b..ff324c9 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,3 +1,22 @@ +GREP + +Были рассмотрены ряд библиотек для парсинга аргументов: +* argparse +* optparse +* click +* docopt + +В результате был выбран argparse, по ряду причин: +* встроен в стандартную библиотеку и не требует установки +* нет проблем с лицензией +* большая популярность и множество примеров работы +* поддержка опциональных аргументов +* автогенерируемый help по аргументам и использованию команды + +Все остальные уступают в том или ином виде, в частности некоторые из них уже устарели (optparse) и считаются deprecated + + + Интерпретатор командной строки, поддерживающий следующие команды: * cat [FILE] — вывести на экран содержимое файла; * echo — вывести на экран свой аргумент (или аргументы); @@ -28,4 +47,4 @@ name, который должен вернуть имя команды, по к IExecutor - аккумулирует все вышеперечисленное и должен преобразовывать входное выражение в результат его исполнения, что делает его единственный метод execute_expression -![alt text](./class_diagram.png) \ No newline at end of file +![alt text](./class_diagram.png) diff --git a/cli/cli.py b/cli/cli.py index 1ef06ab..5ad3f63 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -11,7 +11,7 @@ from subprocess import run, PIPE from storage import Storage from commands import CommandCat, CommandEcho, CommandWC, CommandPwd, \ - CommandExit, CommandDefault + CommandExit, CommandDefault, CommandGrep from tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ TokenAssignment, TokenWord from interpreter import CommandInterpreterWithStorage @@ -29,7 +29,7 @@ def main_loop(): storage = Storage(r'\$[^ \'\"$]+') commands = [CommandCat, CommandEcho, CommandWC, - CommandPwd, CommandExit] + CommandPwd, CommandExit, CommandGrep] token_types = [TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, TokenAssignment, TokenWord] diff --git a/cli/commands.py b/cli/commands.py index 8d8290e..a89611d 100644 --- a/cli/commands.py +++ b/cli/commands.py @@ -1,11 +1,13 @@ """ Модуль с командами с которыми работает интерпретатор """ - +import os +import re from abc import ABCMeta, abstractmethod from typing import List from subprocess import run, PIPE from storage import IStorage +import argparse class ICommand(metaclass=ABCMeta): @@ -133,3 +135,60 @@ 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" + + 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.word_regexp: + pattern = r"\b" + pattern + r"\b" + + if args.FILE: + pipe = "" + for filename in args.FILE: + 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 + + 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/grep_test b/cli/grep_test new file mode 100644 index 0000000..899ee49 --- /dev/null +++ b/cli/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/interpreter.py b/cli/interpreter.py index 73afc59..a19506d 100644 --- a/cli/interpreter.py +++ b/cli/interpreter.py @@ -56,7 +56,7 @@ def retrieve_commands(self, tokens: Iterator[IToken]) -> \ token = next(tokens, None) if not token: - raise StopIteration() + return if not token.is_possibly_command(): raise RuntimeError("Unexpected token: " + token.get_value()) diff --git a/cli/tests/test_command_cat.py b/cli/tests/test_command_cat.py index 99e492a..9ebbea8 100644 --- a/cli/tests/test_command_cat.py +++ b/cli/tests/test_command_cat.py @@ -1,13 +1,16 @@ from unittest import TestCase from commands import CommandCat +from storage import Storage class TestCommandCat(TestCase): def test_execute(self): + storage = Storage(r'\$[^ \'\"$]+') + command = CommandCat(['../example.txt']) - self.assertEqual(command.execute(""), "Some example text") + self.assertEqual(command.execute("", storage), "Some example text") command = CommandCat(['dsakfjhakdsljf']) - self.assertEqual(command.execute(""), "cat: 'dsakfjhakdsljf' " - "No such file or directory") + 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 index b00f95d..26feb69 100644 --- a/cli/tests/test_command_echo.py +++ b/cli/tests/test_command_echo.py @@ -1,9 +1,12 @@ from unittest import TestCase from commands import CommandEcho +from storage import Storage class TestCommandEcho(TestCase): def test_execute(self): + storage = Storage(r'\$[^ \'\"$]+') + command = CommandEcho(['123', 'asd']) - self.assertEqual(command.execute(""), "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..6859529 --- /dev/null +++ b/cli/tests/test_command_grep.py @@ -0,0 +1,28 @@ +from unittest import TestCase + +from commands import CommandGrep +from storage import Storage + + +class TestCommandGrep(TestCase): + def test_execute(self): + storage = Storage(r'\$[^ \'\"$]+') + + command = CommandGrep(['-i', '-w', '-A 2', 'plugin', '../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', '../grep_test']) + self.assertEqual(command.execute("", storage), "") + + command = CommandGrep(['-i', '-w', '-A 2', 'PluGin', '../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', '../grep_test']) + self.assertEqual(command.execute("", storage), "apply plugin: 'java'\n" + "apply plugin: 'idea'") diff --git a/cli/tests/test_command_wc.py b/cli/tests/test_command_wc.py index 185e22c..cf777a7 100644 --- a/cli/tests/test_command_wc.py +++ b/cli/tests/test_command_wc.py @@ -1,16 +1,20 @@ from unittest import TestCase from commands import CommandWC +from storage import Storage class TestCommandWC(TestCase): def test_execute(self): + storage = Storage(r'\$[^ \'\"$]+') + command = CommandWC(['../example.txt']) - self.assertEqual(command.execute(""), "1 3 17") + self.assertEqual(command.execute("", storage), "1 3 17") command = CommandWC([]) - self.assertEqual(command.execute('Some example text'), "1 3 17") + self.assertEqual(command.execute('Some example text', storage), + "1 3 17") command = CommandWC(['sdfsdfsdf']) - self.assertEqual(command.execute(''), "wc: 'sdfsdfsdf'" - " No such file or directory") + 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 index eeb15ab..011ab9c 100644 --- a/cli/tests/test_executor.py +++ b/cli/tests/test_executor.py @@ -20,7 +20,7 @@ def test_execute_expression(self): executor = Executor(CommandInterpreterWithStorage (storage, commands, TokenPipe, CommandDefault), - Parser(token_types)) + Parser(token_types), storage) self.assertEqual(executor.execute_expression('echo "Hello, world!"'), 'Hello, world!') From b63a0cdde203c73ed2c5035644ee4161be8b1d39 Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 15:15:55 +0300 Subject: [PATCH 04/15] Create .travis.yml --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a5ecbbc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: python +python: 3.7 +script: pytest From 3249b5cdd5b9f96d80247664a1497548deb2e345 Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 15:19:09 +0300 Subject: [PATCH 05/15] Update .travis.yml --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a5ecbbc..4350181 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ language: python -python: 3.7 +python: + - "3.7-dev" script: pytest From 889001e94f02524de92231dcbeac0a244e70fb55 Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 15:31:01 +0300 Subject: [PATCH 06/15] inits --- cli/__init__.py | 0 cli/tests/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 cli/__init__.py create mode 100644 cli/tests/__init__.py diff --git a/cli/__init__.py b/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/tests/__init__.py b/cli/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 8080b4a140ac52121f9e7f630b08f145a66bb5a9 Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 15:36:33 +0300 Subject: [PATCH 07/15] Update .travis.yml --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4350181..23204f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: python python: - "3.7-dev" +before_script: + - export PYTHONPATH=$PYTHONPATH:$(pwd) script: pytest From a6873059e8492987a95f346c97c867a979dee146 Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 15:41:35 +0300 Subject: [PATCH 08/15] Update .travis.yml --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 23204f8..5a95dfc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - "3.7-dev" -before_script: - - export PYTHONPATH=$PYTHONPATH:$(pwd) -script: pytest +script: + - python3 -m unittest discover tests/ From 4a78cca7d18cd190664b05229a35971f82b523fd Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 15:43:24 +0300 Subject: [PATCH 09/15] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5a95dfc..6ef3910 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,4 @@ language: python python: - "3.7-dev" script: - - python3 -m unittest discover tests/ + - python3 -m unittest discover cli/tests/ From 1d5aca05e6531124007b2f2d32b5f15189b38e1c Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 15:51:31 +0300 Subject: [PATCH 10/15] refactoring --- .travis.yml | 2 +- cli/{ => src}/__init__.py | 0 cli/{ => src}/cli.py | 12 ++++++------ cli/{ => src}/commands.py | 2 +- cli/{ => src}/executor.py | 6 +++--- cli/{ => src}/interpreter.py | 6 +++--- cli/{ => src}/pparser.py | 2 +- cli/{ => src}/storage.py | 0 cli/{ => src}/tokens.py | 2 +- cli/{ => tests}/example.txt | 0 cli/{ => tests}/grep_test | 0 cli/tests/test_command_cat.py | 6 +++--- cli/tests/test_command_echo.py | 4 ++-- cli/tests/test_command_grep.py | 12 ++++++------ cli/tests/test_command_interpreterpy.py | 10 +++++----- cli/tests/test_command_wc.py | 6 +++--- cli/tests/test_executor.py | 14 +++++++------- cli/tests/test_parser.py | 4 ++-- cli/tests/test_storage.py | 2 +- 19 files changed, 45 insertions(+), 45 deletions(-) rename cli/{ => src}/__init__.py (100%) rename cli/{ => src}/cli.py (83%) rename cli/{ => src}/commands.py (99%) rename cli/{ => src}/executor.py (90%) rename cli/{ => src}/interpreter.py (97%) rename cli/{ => src}/pparser.py (98%) rename cli/{ => src}/storage.py (100%) rename cli/{ => src}/tokens.py (99%) rename cli/{ => tests}/example.txt (100%) rename cli/{ => tests}/grep_test (100%) diff --git a/.travis.yml b/.travis.yml index 6ef3910..e543976 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,4 @@ language: python python: - "3.7-dev" script: - - python3 -m unittest discover cli/tests/ + - pytest diff --git a/cli/__init__.py b/cli/src/__init__.py similarity index 100% rename from cli/__init__.py rename to cli/src/__init__.py diff --git a/cli/cli.py b/cli/src/cli.py similarity index 83% rename from cli/cli.py rename to cli/src/cli.py index 5ad3f63..9eb9a40 100644 --- a/cli/cli.py +++ b/cli/src/cli.py @@ -9,14 +9,14 @@ import os from subprocess import run, PIPE -from storage import Storage -from commands import CommandCat, CommandEcho, CommandWC, CommandPwd, \ +from src.storage import Storage +from src.commands import CommandCat, CommandEcho, CommandWC, CommandPwd, \ CommandExit, CommandDefault, CommandGrep -from tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ +from src.tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ TokenAssignment, TokenWord -from interpreter import CommandInterpreterWithStorage -from pparser import Parser -from executor import Executor +from src.interpreter import CommandInterpreterWithStorage +from src.pparser import Parser +from src.executor import Executor def main_loop(): diff --git a/cli/commands.py b/cli/src/commands.py similarity index 99% rename from cli/commands.py rename to cli/src/commands.py index a89611d..dd325fd 100644 --- a/cli/commands.py +++ b/cli/src/commands.py @@ -6,7 +6,7 @@ from abc import ABCMeta, abstractmethod from typing import List from subprocess import run, PIPE -from storage import IStorage +from src.storage import IStorage import argparse diff --git a/cli/executor.py b/cli/src/executor.py similarity index 90% rename from cli/executor.py rename to cli/src/executor.py index 973debf..03ac12b 100644 --- a/cli/executor.py +++ b/cli/src/executor.py @@ -3,9 +3,9 @@ """ from abc import ABCMeta, abstractmethod -from interpreter import ICommandInterpreter -from pparser import IParser -from storage import IStorage +from src.interpreter import ICommandInterpreter +from src.pparser import IParser +from src.storage import IStorage class IExecutor(metaclass=ABCMeta): diff --git a/cli/interpreter.py b/cli/src/interpreter.py similarity index 97% rename from cli/interpreter.py rename to cli/src/interpreter.py index a19506d..87aea9e 100644 --- a/cli/interpreter.py +++ b/cli/src/interpreter.py @@ -4,9 +4,9 @@ from abc import ABCMeta, abstractmethod from typing import List, Type, Iterator, Generator -from tokens import IToken -from commands import ICommand -from storage import IStorage +from src.tokens import IToken +from src.commands import ICommand +from src.storage import IStorage class ICommandInterpreter(metaclass=ABCMeta): diff --git a/cli/pparser.py b/cli/src/pparser.py similarity index 98% rename from cli/pparser.py rename to cli/src/pparser.py index 4bcfea4..d220dfe 100644 --- a/cli/pparser.py +++ b/cli/src/pparser.py @@ -7,7 +7,7 @@ import copy import re from typing import List, Type, Iterator -from tokens import IToken +from src.tokens import IToken class IParser(metaclass=ABCMeta): diff --git a/cli/storage.py b/cli/src/storage.py similarity index 100% rename from cli/storage.py rename to cli/src/storage.py diff --git a/cli/tokens.py b/cli/src/tokens.py similarity index 99% rename from cli/tokens.py rename to cli/src/tokens.py index 2f2529a..6f9aeb7 100644 --- a/cli/tokens.py +++ b/cli/src/tokens.py @@ -3,7 +3,7 @@ """ from abc import ABCMeta, abstractmethod -from storage import IStorage +from src.storage import IStorage class IToken(metaclass=ABCMeta): diff --git a/cli/example.txt b/cli/tests/example.txt similarity index 100% rename from cli/example.txt rename to cli/tests/example.txt diff --git a/cli/grep_test b/cli/tests/grep_test similarity index 100% rename from cli/grep_test rename to cli/tests/grep_test diff --git a/cli/tests/test_command_cat.py b/cli/tests/test_command_cat.py index 9ebbea8..e69cf33 100644 --- a/cli/tests/test_command_cat.py +++ b/cli/tests/test_command_cat.py @@ -1,14 +1,14 @@ from unittest import TestCase -from commands import CommandCat -from storage import Storage +from src.commands import CommandCat +from src.storage import Storage class TestCommandCat(TestCase): def test_execute(self): storage = Storage(r'\$[^ \'\"$]+') - command = CommandCat(['../example.txt']) + command = CommandCat(['example.txt']) self.assertEqual(command.execute("", storage), "Some example text") command = CommandCat(['dsakfjhakdsljf']) diff --git a/cli/tests/test_command_echo.py b/cli/tests/test_command_echo.py index 26feb69..ed9e3af 100644 --- a/cli/tests/test_command_echo.py +++ b/cli/tests/test_command_echo.py @@ -1,7 +1,7 @@ from unittest import TestCase -from commands import CommandEcho -from storage import Storage +from src.commands import CommandEcho +from src.storage import Storage class TestCommandEcho(TestCase): diff --git a/cli/tests/test_command_grep.py b/cli/tests/test_command_grep.py index 6859529..c543129 100644 --- a/cli/tests/test_command_grep.py +++ b/cli/tests/test_command_grep.py @@ -1,28 +1,28 @@ from unittest import TestCase -from commands import CommandGrep -from storage import Storage +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', '../grep_test']) + command = CommandGrep(['-i', '-w', '-A 2', 'plugin', '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', '../grep_test']) + command = CommandGrep(['-i', '-w', '-A 2', 'plugi', 'grep_test']) self.assertEqual(command.execute("", storage), "") - command = CommandGrep(['-i', '-w', '-A 2', 'PluGin', '../grep_test']) + command = CommandGrep(['-i', '-w', '-A 2', 'PluGin', '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', '../grep_test']) + command = CommandGrep(['plugin', '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 index 74ebd78..e3b87e1 100644 --- a/cli/tests/test_command_interpreterpy.py +++ b/cli/tests/test_command_interpreterpy.py @@ -1,11 +1,11 @@ from unittest import TestCase -from commands import CommandCat, CommandExit, CommandEcho, CommandWC, \ +from src.commands import CommandCat, CommandExit, CommandEcho, CommandWC, \ CommandPwd, CommandDefault -from interpreter import CommandInterpreterWithStorage -from pparser import Parser -from storage import Storage -from tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ +from src.interpreter import CommandInterpreterWithStorage +from src.pparser import Parser +from src.storage import Storage +from src.tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ TokenAssignment, TokenWord diff --git a/cli/tests/test_command_wc.py b/cli/tests/test_command_wc.py index cf777a7..3ff15ac 100644 --- a/cli/tests/test_command_wc.py +++ b/cli/tests/test_command_wc.py @@ -1,14 +1,14 @@ from unittest import TestCase -from commands import CommandWC -from storage import Storage +from src.commands import CommandWC +from src.storage import Storage class TestCommandWC(TestCase): def test_execute(self): storage = Storage(r'\$[^ \'\"$]+') - command = CommandWC(['../example.txt']) + command = CommandWC(['example.txt']) self.assertEqual(command.execute("", storage), "1 3 17") command = CommandWC([]) diff --git a/cli/tests/test_executor.py b/cli/tests/test_executor.py index 011ab9c..80031d9 100644 --- a/cli/tests/test_executor.py +++ b/cli/tests/test_executor.py @@ -1,13 +1,13 @@ from unittest import TestCase -from storage import Storage -from commands import CommandCat, CommandEcho, CommandWC, CommandPwd, \ +from src.storage import Storage +from src.commands import CommandCat, CommandEcho, CommandWC, CommandPwd, \ CommandExit, CommandDefault -from tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ +from src.tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ TokenAssignment, TokenWord -from interpreter import CommandInterpreterWithStorage -from pparser import Parser -from executor import Executor +from src.interpreter import CommandInterpreterWithStorage +from src.pparser import Parser +from src.executor import Executor class TestExecutor(TestCase): @@ -25,7 +25,7 @@ def test_execute_expression(self): self.assertEqual(executor.execute_expression('echo "Hello, world!"'), 'Hello, world!') - self.assertEqual(executor.execute_expression('FILE=../example.txt'), '') + self.assertEqual(executor.execute_expression('FILE=example.txt'), '') self.assertEqual(executor.execute_expression('cat $FILE'), 'Some example text') diff --git a/cli/tests/test_parser.py b/cli/tests/test_parser.py index a22f8a2..cd3e4d1 100644 --- a/cli/tests/test_parser.py +++ b/cli/tests/test_parser.py @@ -1,7 +1,7 @@ from unittest import TestCase -from pparser import Parser -from tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ +from src.pparser import Parser +from src.tokens import TokenInSingleQuotes, TokenInDoubleQuotes, TokenPipe, \ TokenAssignment, TokenWord diff --git a/cli/tests/test_storage.py b/cli/tests/test_storage.py index cf1109d..cd1e424 100644 --- a/cli/tests/test_storage.py +++ b/cli/tests/test_storage.py @@ -1,6 +1,6 @@ from unittest import TestCase -from storage import Storage +from src.storage import Storage class TestStorage(TestCase): From e1a1ed742c0a12685221c09ab84265d73eb8ba23 Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 15:56:58 +0300 Subject: [PATCH 11/15] [==] --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e543976..99eb8b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,4 @@ language: python python: - "3.7-dev" script: - - pytest + - pytest cli/tests From bc3f1aa29528e4b147eb9a4d54dd70c53db111a0 Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 16:00:48 +0300 Subject: [PATCH 12/15] [==] --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 99eb8b9..ac1cc59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,4 @@ language: python python: - "3.7-dev" script: - - pytest cli/tests + - pytest --rootdir=/home/travis/build/Fatalll/Software_Design/cli/tests From 7b4320afbc326ff28c8f8039f494446774fa1b63 Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 16:34:35 +0300 Subject: [PATCH 13/15] [==] --- .travis.yml | 2 +- cli/src/__init__.py | 0 cli/tests/__init__.py | 0 cli/tests/test_command_cat.py | 3 ++- cli/tests/test_command_grep.py | 13 +++++++++---- cli/tests/test_command_wc.py | 3 ++- cli/tests/test_executor.py | 4 +++- 7 files changed, 17 insertions(+), 8 deletions(-) delete mode 100644 cli/src/__init__.py delete mode 100644 cli/tests/__init__.py diff --git a/.travis.yml b/.travis.yml index ac1cc59..e543976 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,4 @@ language: python python: - "3.7-dev" script: - - pytest --rootdir=/home/travis/build/Fatalll/Software_Design/cli/tests + - pytest diff --git a/cli/src/__init__.py b/cli/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cli/tests/__init__.py b/cli/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cli/tests/test_command_cat.py b/cli/tests/test_command_cat.py index e69cf33..828b81c 100644 --- a/cli/tests/test_command_cat.py +++ b/cli/tests/test_command_cat.py @@ -1,3 +1,4 @@ +import os from unittest import TestCase from src.commands import CommandCat @@ -8,7 +9,7 @@ class TestCommandCat(TestCase): def test_execute(self): storage = Storage(r'\$[^ \'\"$]+') - command = CommandCat(['example.txt']) + command = CommandCat([os.path.dirname(__file__) + '/example.txt']) self.assertEqual(command.execute("", storage), "Some example text") command = CommandCat(['dsakfjhakdsljf']) diff --git a/cli/tests/test_command_grep.py b/cli/tests/test_command_grep.py index c543129..9d847e0 100644 --- a/cli/tests/test_command_grep.py +++ b/cli/tests/test_command_grep.py @@ -1,3 +1,4 @@ +import os from unittest import TestCase from src.commands import CommandGrep @@ -8,21 +9,25 @@ class TestCommandGrep(TestCase): def test_execute(self): storage = Storage(r'\$[^ \'\"$]+') - command = CommandGrep(['-i', '-w', '-A 2', 'plugin', 'grep_test']) + 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', 'grep_test']) + 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', 'grep_test']) + 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', 'grep_test']) + 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_wc.py b/cli/tests/test_command_wc.py index 3ff15ac..d32c949 100644 --- a/cli/tests/test_command_wc.py +++ b/cli/tests/test_command_wc.py @@ -1,3 +1,4 @@ +import os from unittest import TestCase from src.commands import CommandWC @@ -8,7 +9,7 @@ class TestCommandWC(TestCase): def test_execute(self): storage = Storage(r'\$[^ \'\"$]+') - command = CommandWC(['example.txt']) + command = CommandWC([os.path.dirname(__file__) + '/example.txt']) self.assertEqual(command.execute("", storage), "1 3 17") command = CommandWC([]) diff --git a/cli/tests/test_executor.py b/cli/tests/test_executor.py index 80031d9..6c52da0 100644 --- a/cli/tests/test_executor.py +++ b/cli/tests/test_executor.py @@ -1,3 +1,4 @@ +import os from unittest import TestCase from src.storage import Storage @@ -25,7 +26,8 @@ def test_execute_expression(self): self.assertEqual(executor.execute_expression('echo "Hello, world!"'), 'Hello, world!') - self.assertEqual(executor.execute_expression('FILE=example.txt'), '') + self.assertEqual(executor.execute_expression( + 'FILE=' + os.path.dirname(__file__) + '/example.txt'), '') self.assertEqual(executor.execute_expression('cat $FILE'), 'Some example text') From a9ac7a4ea53591ae041519a4e862513c34b38a2f Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Fri, 1 Mar 2019 16:36:43 +0300 Subject: [PATCH 14/15] [==] --- cli/src/__init__.py | 0 cli/tests/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 cli/src/__init__.py create mode 100644 cli/tests/__init__.py diff --git a/cli/src/__init__.py b/cli/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/tests/__init__.py b/cli/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 605fe218c9a475f8d96d99ef63dc005435fc5823 Mon Sep 17 00:00:00 2001 From: Pavel Bakhvalov Date: Sat, 8 Jun 2019 16:09:21 +0300 Subject: [PATCH 15/15] fix lines NUM after context negative integer --- cli/src/commands.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/cli/src/commands.py b/cli/src/commands.py index dd325fd..a27c8e0 100644 --- a/cli/src/commands.py +++ b/cli/src/commands.py @@ -144,6 +144,19 @@ class CommandGrep(ICommand): 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') @@ -164,17 +177,16 @@ def execute(self, pipe: str, storage: IStorage) -> str: 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: - pipe = "" - for filename in args.FILE: - 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 + res, pipe = self.read_from_file(args.FILE) + result += res after_lines_count = 0 for line in pipe.splitlines(True):