Все статьи Автоматизируем вызов graphviz из командной строки
Graphviz — утилита командной строки для генерации схем графов на основе текстовых описаний узлов и рёбер. Из статьи вы узнаете, как и языка C++ программно вызвать graphiz с заданными параметрами, прочитать вывод и код возврата вызванной утилиты. Способ можно использовать в отрыве от Graphviz, здесь он взят для примера.
Установка graphviz
Чтобы получить graphviz на Windows, вам нужно:
- установить Graphviz из MSI-пакета с сайта graphviz.org
- добавить в системную переменную PATH путь к Graphviz (полный путь к поддиректории
Graphviz2.38\bin
внутриProgram Files
)
Для Ubuntu потребуется:
- установить пакет dot
После установки проверьте работу graphviz на простом примере:
digraph G {
0 [shape="circle"label="S0(a)"];
1 [shape="box"label="S1(b)"];
2 [shape="box"label="S2(c)"];
0->1 [label=" a "];
0->2 [label=" b "];
1->1 [label=" b "];
2->1 [label=" a "];
}
Автоматизируем вызов Graphviz
Для автоматизации нам потребуется кроссплатформенный способ запуска внешнего процесса из C++. Например, функции popen
/pclose
из стандарта POSIX.
На Windows название функции popen
изменено на _popen
. Это легко исправить с помощью препроцессора:
макрозамена popen/pclose
#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#endif
Используя пример из документации (msdn.microsoft.com), можно реализовать функцию для запуска произвольной команды, печати вывода в консоль и возврата true/false при успешном/неуспешном выполнении.
объявление класса CUtils
#pragma once
#include <string>
class CUtils
{
public:
CUtils() = delete;
static bool RunProcess(std::string const& command);
};
запуск внешней команды в CUtils::RunProcess
bool CUtils::RunProcess(const std::string &command)
{
CPipeHandle pipe(popen(command.c_str(), "r"));
if (!pipe)
{
return false;
}
std::vector<char> buffer(256);
while (std::fgets(buffer.data(), int(buffer.size()), pipe))
{
std::fputs(buffer.data(), stderr);
}
if (std::feof(pipe))
{
return true;
}
// process run failed, print error.
std::fputs(std::strerror(errno), stderr);
return false;
}
Класс CPipeHandle — это простейшая безопасная обёртка над FILE*
, в деструкторе которой автоматически вызывается закрытие канала через pclose
.
RAII-обёртка для FILE*
namespace
{
class CPipeHandle
{
public:
CPipeHandle(FILE *pipe)
: m_pipe(pipe)
{
}
~CPipeHandle()
{
if (m_pipe)
{
pclose(m_pipe);
}
}
operator FILE*()const
{
return m_pipe;
}
private:
FILE *m_pipe = nullptr;
};
}
Чтобы воспользоваться API, нужно указать полную команду со всеми аргументами. Если путь к исполняемому файлу команды не добавлен в переменную окружения PATH, то придётся указать полный путь к .exe
. Вот пример вызова Graphviz для преобразования диаграммы fsm.dot
в изображение fsm.png
CUtils::RunProcess("dot -Tpng -ofsm.png fsm.dot");
Класс генерации кода для Graphviz
Допустим, нам захочется сделать сериализацию дерева или графа в язык Graphviz, чтобы автоматизировать их визуализацию. Допустим, мы хотим визуализировать конечный автомат:
На выходе мы должны получить примерно такой файл *.dot
:
digraph G {
0 [shape="circle"label="S0(a)"];
1 [shape="box"label="S1(b)"];
2 [shape="box"label="S2(c)"];
0->1 [label=" a "];
0->2 [label=" b "];
1->1 [label=" b "];
2->1 [label=" a "];
}
В файле указаны надписи на вершинах и рёбрах графа, где под вершинами подразумеваются состояния, а под рёбрами — переходы. Для вершин указана форма: для начального состояния круг (circle), для конечных состояний (терминалов) двойной круг (doublecircle), для промежуточных состояний (нетерминалов) прямоугольник (box).
Вспомогательный класс для сериализации назовём CDotWriter. Определение класса:
enum class StateType
{
Initial,
Nonterminal,
Terminal
};
class CDotWriter
{
public:
CDotWriter(std::ostream & out);
~CDotWriter();
void PrintVertex(size_t index, std::string const& label, StateType type = StateType::Nonterminal);
void PrintEdge(size_t from, size_t to, std::string const& label);
private:
std::string GetShape(StateType type)const;
std::ostream & m_out;
};
Реализации методов выглядят так:
CDotWriter::CDotWriter(std::ostream &out)
: m_out(out)
{
m_out << "digraph G {\n";
}
CDotWriter::~CDotWriter()
{
m_out << "}\n";
}
void CDotWriter::PrintVertex(size_t index, const std::string &label, StateType type)
{
m_out << index << " [";
m_out << "shape=\"" << GetShape(type) << "\"";
m_out << "label=\"" << label << "\"];\n";
}
void CDotWriter::PrintEdge(size_t from, size_t to, const std::string &label)
{
m_out << from << "->" << to << " [label=\" " << label << " \"];\n";
}
std::string CDotWriter::GetShape(StateType type) const
{
switch (type)
{
case StateType::Initial:
return "circle";
case StateType::Nonterminal:
return "box";
case StateType::Terminal:
return "doublecircle";
}
return std::string();
}