Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
cmake_minimum_required(VERSION 3.15)
project(CommandParser)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

file(GLOB SOURCES "src/*.cpp")

# add_custom_target(run COMMAND ${CMAKE_CURRENT_BINARY_DIR}/${EXECUTABLE} 'echo million | cat ../static/text.txt | cat ../static/zarplata.txt | wc -c | echo rambler | cat ../static/zarplata.txt | wc -c | cat ../static/text.txt | wc -c')
# MakeFile удобнее, так как там я мог make run сразу запускать файл с переданной строкой

add_executable(${PROJECT_NAME} ${SOURCES})

target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)

target_link_libraries(${PROJECT_NAME})


# Автоматический запуск тестов после сборки
add_custom_command(
TARGET ${PROJECT_NAME}
POST_BUILD
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
)
21 changes: 21 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
CXX := g++
CXX_FLAGS := -Wall -Wextra -std=c++20 -ggdb

BIN := bin
SRC := src
INCLUDE := include
LIB := lib

LIBRARIES :=
EXECUTABLE := main

all: $(BIN)/$(EXECUTABLE)

run: clean all
./$(BIN)/$(EXECUTABLE) 'echo million | cat static/text.txt | cat static/zarplata.txt | wc -c | echo rambler | cat static/zarplata.txt | wc -c | cat static/text.txt | wc -c'

$(BIN)/$(EXECUTABLE): $(wildcard $(SRC)/*.cpp)
$(CXX) $(CXX_FLAGS) -I$(INCLUDE) -L$(LIB) $^ -o $@ $(LIBRARIES)

clean:
-rm -f $(BIN)/*
50 changes: 28 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,40 @@
## Platforms
- Linux Ubuntu 22.04
---
### DZ1 Вариант #13
> Parse file - How many groups from place
- Необходимо разработать программу, которая обрабатывает данные онлайн сервиса MusicBrainz.
Датасет можно скачать на официальном сайте MusicBrainz https://data.metabrainz.org/pub/musicbrainz/data/fullexport/
Необходимый файл mbdump.tar.bz2.
Описание датасета находится по ссылке https://musicbrainz.org/doc/MusicBrainz_Database/Schema
Например, для Артиста диаграмма находится по ссылке https://wiki.musicbrainz.org/images/7/7e/artist_entity_details.svg
Файл датасета представляет из себя сжатый архив из множества текстовых файлов, где данные располагаются в строчках, а поля разделены символом табуляции \t
Каждая таблица на схемах представлена отдельным файлом с аналогичным названием
### DZ1 Вариант #11
- ​Необходимо написать программу, реализующую функционал потокового текстового редактора с использованием ООП.
Необходимо реализовать поддержку операций:
cat <filename> - передает следующей операции сначала строки поданные на вход, затем строки из файла filename
echo <some string> - игнорирует строки поданные на вход, передает следующей операции строку <some string>
Выход одной операции подается на вход другой операции, операции разделяются символом '|'.
Если следующей операции нет, то строки выводятся в стандартный вывод (на экран).

- Скачать датасет по ссылке можно с помощью команды wget, например
wget https://data.metabrainz.org/pub/musicbrainz/data/fullexport/20230304-002037/mbdump.tar.bz2
Полученный файл нужно распаковать
tar xvf mbdump.tar.bz2
Все файлы кроме gender, area, artist, artist_type можно удалить с помощью команды rm.
Программа должна принимать необходимые для работы имена распакованных файлов в качестве аргументов командной строки и выводить данные в стандартный вывод.
Программа не должна использовать интерактивный ввод с клавиатуры, например, "введите имя файла", "введите необходимый год", "ведите q чтобы выйти" - такими программами не удобно пользоваться и их разработка занимает больше времени.

- Для реализации обработки строк утверждается следующий интерфейс IOperation.
Наследник IOperation обязан реализовать методы:
void ProcessLine(const std::string& str);
void HandleEndOfInput() = 0;
void SetNextOperation(<умный/сырой указатель/ссылка на IOperation>) = 0;

- Каждая операция наследуется от интерфейса IOperation.
Программа должна парсить выражение, переданное в аргументах командной строки и строить конвеер из наследников IOperation.
Выражение можно парсить предельно строго ( например, недопускаются лишние пробелы или их отсутствие )
Для упрощения парсера аргументы отдельных операций не допускают пробелы ( echo 1 2 3 недопустим как и cat file name.txt )
Результат вычисления получается вызовом метода HandleEndOfInput у первой операции.
Пример (при вводе в терминале выражение с пробелами нужно обернуть, например, в одинарные кавычки)
./text_processor 'echo 1 | cat file_1.txt | cat file_2.txt'
1
<содержимое file_1.txt>
<содержимое file_2.txt>

- Ваш вариант:
Вывести число групп созданных в месте, название которого передано в аргументах командной строки
Добавьте к существующим операциям wc -c - операцию, которая выводит или передает дальше число суммарное число байт во всех переданных на вход строках
#### Run application
```
./bin/main <Place_name>
./bin/main <Operation chain>
```
#### For example (With output):
#### For example:
```
./bin/main "Chile"
Input: Chile, Id = 43
Groups from this place = 828
./bin/main 'echo million$ | cat text.txt | cat zarplata.txt'
```
---
17 changes: 17 additions & 0 deletions include/CatOperation.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once
#include <iostream>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вот здесь и во многих других местах, по-хорошему нужно убрать или перенести хедеры в файлы, где они действительно нужны. В этом файле, кстати, нужен только IOperation.hpp, все остальное или уже определено в IOperation.hpp или нужно, но не здесь, а например, в CatOperation.cpp

#include <memory>
#include <fstream>
#include "IOperation.hpp"

class CatOperation final : public IOperation {
public:
CatOperation() : IOperation() {}
CatOperation(std::string& filename, std::shared_ptr<IOperation>&& operation=nullptr) : IOperation(std::move(operation)), filename_(filename) {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

По-моему в коде многовато "лишних" конструкторов. Например, кажется, что не нужен обычный конструктор у CatOperation(). Не вижу, где используется. Так же "хитрый" второй конструктор, с дефолтное некст-операцией, вроде тоже не нужен такой. Если где-то что-то не используется, то следует подчистить.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Реализация таких конструкторов подразумевает разное использование классов. Конструктор где второй параметр нулевой используется везде, это самый главный конструктор

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я имею в виду: а есть ли хотя бы одно место, где этот конструктор используется с двумя параметрами? Если нет, то нужно оставить только один параметр filename и дело с концом. Кстати, почему он не const std::string& ?
Я имею в виду, что если что-то не используется, то оно должно быть удалено.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А если мне потом понадобится передать сразу при конструкторе следующую операцию? Я буду вынужден залезать в код и редактировать. Зачем? Сразу надо предусматривать удобство

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обычно при таком подходе происходит немного не так:

  1. Код так и остается неиспользованным.
  2. Когда действительно понадобится что-то поменять, то скорее всего не именно здесь, а немного рядом (например, действительно потребуется второй параметр, но другой), и в этом случае придется не только добавлять второй нужный параметр, но и продолжать поддерживать ненужный третий.
  3. Читается такой код хуже. Будет потрачено время на анализ, что это и для чего. (не обязательно к этому случаю, а просто к любому неиспользумому коду). Если потребуется код "переписать", то много усилий стоит понять, для чего же нужен код, который не используется, и можно ли его просто удалить.
  4. Работает ли правильно неиспользуемый код - большой вопрос. Можно ли его просто взять и использовать. Я имею в виду случай, когда программа давно живет "в продакшне", и в ней нужно что-то починить или добавить. Используемый код так или иначе проверен реальной работой.
  5. Если оставить, допустим, такую возможность в конструкторе. То почему не добавить еще несколько конструкторов? Если мы пишем библиотеку, вроде STL, то, конечно, нам надо предоставить все виды конструкторов, которые могли бы быть полезны пользователю. Но в нашем случае мы пишем прикладную программу.

Ну т.е. по многим причинам люди обычно не пишут "на будущее", "для удобства", если не знают четкого плана, когда это действительно будет нужно. Неиспользуемый код делает программу сложнее, больше, менее читаемой и сложнее поддерживаемой.


void ProcessLine(const std::string& pipeline) override;
void HandleEndOfInput() override;

private:
std::string filename_;
};
25 changes: 25 additions & 0 deletions include/CommandParser.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include "IOperation.hpp"
#include "EchoOperation.hpp"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommandParser.hpp-заголовку не нужно знать, какие именно конкретные операции есть в программе. А заодно эти операции не нужны main.cpp, который инклудит этот CommandParser.hpp. Реально, эти заголовки нужны только CommandParser.cpp. Значит, следует туда и перенести. Это уменьшит зависимость внутри проекта, т.е. например, при добавлении нового оператора, main.cpp не нужно будет пересобирать. Т.е. скорость сборки увеличивает, размер программы итоговой уменьшает. В идеале сущности должны видеть только то, что действительно используют. Это касается и других заголовков: разного рода memory и iostream. Если конкретно здесь оно не используется - значит можно удалять. iostream, в этом файле тоже не нужен.

#include "CatOperation.hpp"
#include "WcOperation.hpp"

class OperationsList {
public:
OperationsList() : pipeline_(), head_(), tail_() {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Если такой конструктор не используется в коде, то и не нужно его явно определять. То есть я бы удалил.
Даже если бы такой вариант конструктора был бы востребован, то я бы удалил лист инициализации после двоеточия. Ведь каждый из этих параметров имеет известный нам конструктор по умолчанию, который не нужно явно вызывать, и значения по умолчанию всех 3х полей нам известны.

// input копируем, так как в далньейшем в функции он будет меняться
OperationsList(const std::string& input);

void AddOperation(std::shared_ptr<IOperation>&& operation);
void AddOperation(const std::string& token);
void RunOperations();

private:
const std::string pipeline_;
const std::string delimiter = " | ";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нет смысла хранить эту константу внутри класса. Ведь когда мы создаем экземпляр этого класса, эта константа будет частью получившегося объекта. И если мы создадим 2 экземпляра, то константа будет продублирована. Лучше всего этот delimeter отправить в cpp файл, где он используется, и там его можно или его вытащить в глобальные константы, или проинициализировать внутри какой-то функции, но сделаеть ее static. Оба этих варианта исключат дублирование константы в коде.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Просто сделаю ее статичной внутри класса. Тогда дублирования не будет, верно?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Верно, не будет. Но если выбирать, где лучше сделать ее статической: статическим полем класса или статической локальной переменной в одной каком-то методе в cpp-файле, то я бы выбрал второе. В этом случае мы как бы больше "скрываем" деталей реализации от других. То есть следуем правилу, по которому сущности должны видеть только то, что используют, и то, что им необходимо видеть. На практике это могло бы пригодиться в такой ситуации: решили изменить значение этого разделителя, заменили | на какой-нибудь /. Если константа определена в cpp-файле, то main.cpp перекомпилировать не пришлось бы, тк с его точки зрения, ничего не изменилось, тк он и не видел ничего.

std::shared_ptr<IOperation> head_;
std::shared_ptr<IOperation> tail_;
};
16 changes: 16 additions & 0 deletions include/EchoOperation.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once
#include <iostream>
#include <memory>
#include "IOperation.hpp"

class EchoOperation final : public IOperation {
public:
EchoOperation() : IOperation() {}
EchoOperation(std::string& str, std::shared_ptr<IOperation>&& operation=nullptr) : IOperation(std::move(operation)), str_(str) {}

void ProcessLine(const std::string& pipeline) override;
void HandleEndOfInput() override;

private:
std::string str_;
};
17 changes: 17 additions & 0 deletions include/IOperation.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once
#include <iostream>
#include <memory>

class IOperation {
public:
IOperation() : next_operation_() {}
IOperation(std::shared_ptr<IOperation>&& operation) : next_operation_(std::move(operation)) {}

virtual void ProcessLine(const std::string& pipeline) = 0;
virtual void HandleEndOfInput() = 0;
virtual void SetNextOperation(std::shared_ptr<IOperation>&& operation);

virtual ~IOperation() {}

std::shared_ptr<IOperation> next_operation_;
};
18 changes: 18 additions & 0 deletions include/WcOperation.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#pragma once
#include <iostream>
#include <memory>
#include <fstream>
#include <sstream>
#include "IOperation.hpp"

class WcOperation final : public IOperation {
public:
WcOperation() : IOperation() {}
WcOperation(std::string& str, std::shared_ptr<IOperation>&& operation=nullptr) : IOperation(std::move(operation)), str_(str) {}

void ProcessLine(const std::string& pipeline) override;
void HandleEndOfInput() override;

private:
std::string str_;
};
28 changes: 28 additions & 0 deletions src/CatOperation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include "CatOperation.hpp"
#include <sstream>

void CatOperation::ProcessLine(const std::string& pipeline) {
std::ifstream file_(filename_);
if (!file_.is_open()) {
throw std::runtime_error("Cannot open file: " + filename_);
}

// создаем std::stringstream и передаем в него буфер файла
std::stringstream file_stream;
file_stream << file_.rdbuf();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 слова о подводных камнях.
Вижу, что этот вариант cat-а читает файл целиком, а потом целиком отправляет его дальше. Настоящий линуксовый кат, читает файл все-таки построчно, и построчно отправляет его на вывод. Благодаря этой особенности, можно дампить гигантские файлы, или вообще обрабатывать гигантские файлы, например грепом, построчно. Если прочитать слишком большой файл, то можно "надорваться".


// получаем содержимое файла в виде строки
std::string file_contents = file_stream.str();

// передаем pipeline + file_contents в метод ProcessLine следующей операции
if (next_operation_) {
next_operation_->ProcessLine(pipeline + file_contents);
} else {
std::cout << pipeline << std::endl << file_contents << std::endl;
}
}


void CatOperation::HandleEndOfInput() {
ProcessLine("");
}
66 changes: 66 additions & 0 deletions src/CommandParser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#include "CommandParser.hpp"

OperationsList::OperationsList(const std::string& input) : pipeline_(input), head_(nullptr), tail_(nullptr) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я бы не стал явно инициализировать поля, для которых мы и так знаем, что делает конструктор по умолчанию. Т.е. head_ и tail_ и так понятно, что будут нулями. Можно убрать из листа явной инициализации. (Другое дело, когда мы имеем дело, с какими-нибудь интами, и там не опредлено, какое значение будет в поле, если его явно не инициализировали, да потом еще и обратились к ней, не проинциализировав.)

size_t pos = 0;
std::string token;
std::string_view cursor(pipeline_);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я вижу, что pipeline_ используется только 1 раз, в конструкторе. Если так, то зачем его хранить, тратить место, и ломать голову, зачем он нужен. Не стоит добавлять в класс методы и поля "на будущее". Если они не используются - то надо просто удалить. Так код становится более читаемым и оптимальным.


// Пока есть разделители, запоминаем позицию
while ((pos = cursor.find(delimiter)) != std::string::npos) {
// Получаем подстроку до разделителя
token = cursor.substr(0, pos);
// Удаляем ее из pipeline_ учитывая длину разделителя
cursor = cursor.substr(pos + delimiter.length());
AddOperation(token);
}

// Последняя команда указана без разделителя, обрабатываем ее
if (!cursor.empty()) {
AddOperation(std::string(cursor));
}
}

void OperationsList::AddOperation(const std::string& token){
const std::string ECHO = "echo";
Copy link
Collaborator

@ikramichev ikramichev Apr 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вот эти константы лучше или отправить вне функции просто наверх, или оставить здесь, но static. Так они не будут создаваться и удаляться при каждой AddOperation.

А еще лучше не использовать капслок для обычных констант. Это похоже на дефайны, но все-таки не дефайны.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А какие константы считаются необычными?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Неверно выразился. Я имею в виду любые константы. В противовес старым дефайнам. Раньше в Сишном коде повсюду были дефайны и разные макросы. Сейчас уже язык богаче, возможностей больше, и использовать старомодные дефайны считается "моветоном". А капслок в названии - это некое подражание старому стилю. Но его использовать сегодня незачем, капслок - только для дефайнов, а вместо дефайнов теперь константы.

const std::string CAT = "cat";
const std::string WC = "wc -c";

if (token.starts_with(ECHO)) {
std::string str = token.substr(ECHO.length() + 1); // Достаем переданную строку
AddOperation(std::make_shared<EchoOperation>(str));
return;
}

if (token.starts_with(CAT)) {
std::string file = token.substr(CAT.length() + 1); // Достаем переданный файл
AddOperation(std::make_shared<CatOperation>(file));
return;
}

if (token.starts_with(WC)) {
std::string str;
if (token.length() > WC.length()) {
str = token.substr(WC.length() + 1); // Достаем переданную строку
}
AddOperation(std::make_shared<WcOperation>(str));
return;
}
}


void OperationsList::AddOperation(std::shared_ptr<IOperation>&& operation) {
if (!head_) {
head_ = std::move(operation);
tail_ = head_;
} else {
tail_->SetNextOperation(std::move(operation));
tail_ = tail_->next_operation_;
}
}


void OperationsList::RunOperations() {
if (head_) {
head_->HandleEndOfInput();
}
}
14 changes: 14 additions & 0 deletions src/EchoOperation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#include "EchoOperation.hpp"

void EchoOperation::ProcessLine(const std::string& pipeline) {
if(next_operation_) {
next_operation_->ProcessLine(str_);
} else {
std::cout << str_ << std::endl;
}
}


void EchoOperation::HandleEndOfInput() {
ProcessLine("");
}
6 changes: 6 additions & 0 deletions src/IOperation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include "IOperation.hpp"

// Реализуем по умолчанию метод интерфейсного класса
void IOperation::SetNextOperation(std::shared_ptr<IOperation>&& operation) {
next_operation_ = std::move(operation);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Хорошая идея выделить общий код в базовый класс, чтобы избежать дублирования в каждом из наследников, где этот же код нужен именно в таком виде.
Но в данном случае, чище было бы оставить интерфейсный класс вообще без определений, т.е. эту функцию оставить = 0, а вот между IOperation и конкретными операциями сделать промежуточный класс, в котором был бы собран весь общий код для всех операций. В результате решение было бы гибче.

}
18 changes: 18 additions & 0 deletions src/WcOperation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include "WcOperation.hpp"

void WcOperation::ProcessLine(const std::string& pipeline) {
// вычисляем суммарное число байт в строке full_string = pipeline + str_
int total_bytes = sizeof(char) * (pipeline.size() + str_.size());

// передаем pipeline + str_ в метод ProcessLine следующей операции
if (next_operation_) {
next_operation_->ProcessLine(std::to_string(total_bytes));
} else {
std::cout << total_bytes << std::endl;
}
}


void WcOperation::HandleEndOfInput() {
ProcessLine("");
}
20 changes: 20 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include <iostream>
#include "CommandParser.hpp"


int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Run program with arguments like: 'echo <someText> | cat <someFileName> | echo million | cat Elon.txt'\n";
return 1;
}
try{
OperationsList list(argv[1]);
list.RunOperations();
}
catch(const std::exception& e){
std::cerr << e.what() << '\n';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retrun 1; (раз какая-то ошибка)

}


return 0;
}
Loading