621 lines
20 KiB
C++
621 lines
20 KiB
C++
/**
|
|
* @file src/options/cli_handler.cc
|
|
*
|
|
* Copyright (c) 2021-2023 Bartek Kryza <bkryza@gmail.com>
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
#include "cli_handler.h"
|
|
|
|
#include "class_diagram/generators/plantuml/class_diagram_generator.h"
|
|
#include "include_diagram/generators/plantuml/include_diagram_generator.h"
|
|
#include "package_diagram/generators/plantuml/package_diagram_generator.h"
|
|
#include "sequence_diagram/generators/plantuml/sequence_diagram_generator.h"
|
|
#include "util/util.h"
|
|
#include "version.h"
|
|
|
|
#include <clang/Basic/Version.h>
|
|
#include <clang/Config/config.h>
|
|
#include <indicators/indicators.hpp>
|
|
|
|
namespace clanguml::cli {
|
|
cli_handler::cli_handler(
|
|
std::ostream &ostr, std::shared_ptr<spdlog::logger> logger)
|
|
: ostr_{ostr}
|
|
, logger_{std::move(logger)}
|
|
{
|
|
}
|
|
|
|
void cli_handler::setup_logging()
|
|
{
|
|
spdlog::drop("clanguml-logger");
|
|
|
|
if (!progress) {
|
|
spdlog::register_logger(logger_);
|
|
}
|
|
else {
|
|
// Setup null logger for clean progress indicators
|
|
std::vector<spdlog::sink_ptr> sinks;
|
|
logger_ = std::make_shared<spdlog::logger>(
|
|
"clanguml-logger", begin(sinks), end(sinks));
|
|
spdlog::register_logger(logger_);
|
|
}
|
|
|
|
logger_->set_pattern("[%^%l%^] [tid %t] %v");
|
|
|
|
if (verbose == 0) {
|
|
logger_->set_level(spdlog::level::err);
|
|
}
|
|
else if (verbose == 1) {
|
|
logger_->set_level(spdlog::level::info);
|
|
}
|
|
else if (verbose == 2) {
|
|
logger_->set_level(spdlog::level::debug);
|
|
}
|
|
else {
|
|
logger_->set_level(spdlog::level::trace);
|
|
}
|
|
}
|
|
|
|
cli_flow_t cli_handler::parse(int argc, const char **argv)
|
|
{
|
|
static const std::map<std::string, clanguml::common::generator_type_t>
|
|
generator_type_names{
|
|
{"plantuml", clanguml::common::generator_type_t::plantuml},
|
|
{"json", clanguml::common::generator_type_t::json}};
|
|
|
|
app.add_option("-c,--config", config_path,
|
|
"Location of configuration file, when '-' read from stdin");
|
|
app.add_option("-d,--compile-database", compilation_database_dir,
|
|
"Location of compilation database directory");
|
|
app.add_option("-n,--diagram-name", diagram_names,
|
|
"List of diagram names to generate");
|
|
app.add_option("-g,--generator", generators,
|
|
"Name of the generator (default: plantuml)")
|
|
->transform(CLI::CheckedTransformer(generator_type_names));
|
|
app.add_option("-o,--output-directory", output_directory,
|
|
"Override output directory specified in config file");
|
|
app.add_option("-t,--thread-count", thread_count,
|
|
"Thread pool size (0 = hardware concurrency)");
|
|
app.add_flag("-V,--version", show_version, "Print version and exit");
|
|
app.add_flag("-v,--verbose", verbose,
|
|
"Verbose logging (use multiple times to increase - e.g. -vvv)");
|
|
app.add_flag(
|
|
"-p,--progress", progress, "Show progress bars for generated diagrams");
|
|
app.add_flag("-q,--quiet", quiet, "Minimal logging");
|
|
app.add_flag("-l,--list-diagrams", list_diagrams,
|
|
"Print list of diagrams defined in the config file");
|
|
app.add_flag("--init", initialize, "Initialize example config file");
|
|
app.add_option("--add-compile-flag", add_compile_flag,
|
|
"Add a compilation flag to each entry in the compilation database");
|
|
app.add_option("--remove-compile-flag", remove_compile_flag,
|
|
"Remove a compilation flag from each entry in the compilation "
|
|
"database");
|
|
#if !defined(_WIN32)
|
|
app.add_option("--query-driver", query_driver,
|
|
"Query the specific compiler driver to extract system paths and add to "
|
|
"compile commands (e.g. arm-none-eabi-g++)");
|
|
#endif
|
|
app.add_option(
|
|
"--add-class-diagram", add_class_diagram, "Add class diagram config");
|
|
app.add_option("--add-sequence-diagram", add_sequence_diagram,
|
|
"Add sequence diagram config");
|
|
app.add_option("--add-package-diagram", add_package_diagram,
|
|
"Add package diagram config");
|
|
app.add_option("--add-include-diagram", add_include_diagram,
|
|
"Add include diagram config");
|
|
app.add_option("--add-diagram-from-template", add_diagram_from_template,
|
|
"Add diagram config based on diagram template");
|
|
app.add_option("--template-var", template_variables,
|
|
"Specify a value for a template variable");
|
|
app.add_flag("--list-templates", list_templates,
|
|
"List all available diagram templates");
|
|
app.add_option("--show-template", show_template,
|
|
"Show specific diagram template definition");
|
|
app.add_flag(
|
|
"--dump-config", dump_config, "Print effective config to stdout");
|
|
app.add_flag("--paths-relative-to-pwd", paths_relative_to_pwd,
|
|
"If true, all paths in configuration files are relative to the $PWD "
|
|
"instead of actual location of `.clang-uml` file.");
|
|
app.add_flag("--no-metadata", no_metadata,
|
|
"Skip metadata (e.g. clang-uml version) from diagrams");
|
|
|
|
try {
|
|
app.parse(argc, argv);
|
|
}
|
|
catch (const CLI::CallForHelp &e) {
|
|
exit(app.exit(e)); // NOLINT(concurrency-mt-unsafe)
|
|
}
|
|
catch (const CLI::Success &e) {
|
|
return cli_flow_t::kExit;
|
|
}
|
|
catch (const CLI::ParseError &e) {
|
|
exit(app.exit(e)); // NOLINT(concurrency-mt-unsafe)
|
|
}
|
|
|
|
if (quiet || dump_config)
|
|
verbose = 0;
|
|
else
|
|
verbose++;
|
|
|
|
if (progress)
|
|
verbose = 0;
|
|
|
|
return cli_flow_t::kContinue;
|
|
}
|
|
|
|
cli_flow_t cli_handler::handle_options(int argc, const char **argv)
|
|
{
|
|
auto res = parse(argc, argv);
|
|
|
|
if (res != cli_flow_t::kContinue)
|
|
return res;
|
|
|
|
setup_logging();
|
|
|
|
res = handle_pre_config_options();
|
|
|
|
if (res != cli_flow_t::kContinue)
|
|
return res;
|
|
|
|
res = load_config();
|
|
|
|
if (res != cli_flow_t::kContinue)
|
|
return res;
|
|
|
|
res = handle_post_config_options();
|
|
|
|
return res;
|
|
}
|
|
|
|
cli_flow_t cli_handler::handle_pre_config_options()
|
|
{
|
|
if (show_version) {
|
|
return print_version();
|
|
}
|
|
|
|
if ((config_path == "-") &&
|
|
(initialize || add_diagram_from_template ||
|
|
add_class_diagram.has_value() || add_sequence_diagram.has_value() ||
|
|
add_package_diagram.has_value() ||
|
|
add_include_diagram.has_value())) {
|
|
|
|
LOG_ERROR(
|
|
"ERROR: Cannot add a diagram config to configuration from stdin");
|
|
|
|
return cli_flow_t::kError;
|
|
}
|
|
|
|
if (initialize) {
|
|
return create_config_file();
|
|
}
|
|
|
|
if (config_path != "-") {
|
|
if (add_class_diagram) {
|
|
return add_config_diagram(
|
|
clanguml::common::model::diagram_t::kClass, config_path,
|
|
*add_class_diagram);
|
|
}
|
|
|
|
if (add_sequence_diagram) {
|
|
return add_config_diagram(
|
|
clanguml::common::model::diagram_t::kSequence, config_path,
|
|
*add_sequence_diagram);
|
|
}
|
|
|
|
if (add_package_diagram) {
|
|
return add_config_diagram(
|
|
clanguml::common::model::diagram_t::kPackage, config_path,
|
|
*add_package_diagram);
|
|
}
|
|
|
|
if (add_include_diagram) {
|
|
return add_config_diagram(
|
|
clanguml::common::model::diagram_t::kInclude, config_path,
|
|
*add_include_diagram);
|
|
}
|
|
}
|
|
|
|
return cli_flow_t::kContinue;
|
|
}
|
|
|
|
cli_flow_t cli_handler::load_config()
|
|
{
|
|
try {
|
|
config = clanguml::config::load(
|
|
config_path, paths_relative_to_pwd, no_metadata);
|
|
return cli_flow_t::kContinue;
|
|
}
|
|
catch (std::runtime_error &e) {
|
|
LOG_ERROR(e.what());
|
|
}
|
|
|
|
return cli_flow_t::kError;
|
|
}
|
|
|
|
cli_flow_t cli_handler::handle_post_config_options()
|
|
{
|
|
if (dump_config) {
|
|
return print_config();
|
|
}
|
|
|
|
if (list_diagrams) {
|
|
return print_diagrams_list();
|
|
}
|
|
|
|
if (list_templates) {
|
|
return print_diagram_templates();
|
|
}
|
|
|
|
if (show_template) {
|
|
return print_diagram_template(show_template.value());
|
|
}
|
|
|
|
if (config_path != "-" && add_diagram_from_template) {
|
|
return add_config_diagram_from_template(
|
|
config_path, add_diagram_from_template.value(), template_variables);
|
|
}
|
|
|
|
LOG_INFO("Loaded clang-uml config from {}", config_path);
|
|
|
|
//
|
|
// Override selected config options from command line
|
|
//
|
|
if (compilation_database_dir) {
|
|
config.compilation_database_dir.set(
|
|
util::ensure_path_is_absolute(compilation_database_dir.value())
|
|
.string());
|
|
}
|
|
|
|
effective_output_directory = config.output_directory();
|
|
|
|
// Override the output directory from the config
|
|
// with the value from the command line if any
|
|
if (output_directory)
|
|
effective_output_directory = output_directory.value();
|
|
|
|
if (output_directory) {
|
|
config.output_directory.set(
|
|
util::ensure_path_is_absolute(output_directory.value()).string());
|
|
}
|
|
|
|
LOG_INFO("Loading compilation database from {} directory",
|
|
config.compilation_database_dir());
|
|
|
|
if (!ensure_output_directory_exists(effective_output_directory))
|
|
return cli_flow_t::kError;
|
|
|
|
//
|
|
// Append add_compile_flags and remove_compile_flags to the config
|
|
//
|
|
if (add_compile_flag) {
|
|
std::copy(add_compile_flag->begin(), add_compile_flag->end(),
|
|
std::back_inserter(config.add_compile_flags.value));
|
|
config.add_compile_flags.has_value = true;
|
|
}
|
|
|
|
if (remove_compile_flag) {
|
|
std::copy(remove_compile_flag->begin(), remove_compile_flag->end(),
|
|
std::back_inserter(config.remove_compile_flags.value));
|
|
config.remove_compile_flags.has_value = true;
|
|
}
|
|
|
|
#if !defined(_WIN32)
|
|
if (query_driver) {
|
|
config.query_driver.set(*query_driver);
|
|
}
|
|
#endif
|
|
|
|
return cli_flow_t::kContinue;
|
|
}
|
|
|
|
cli_flow_t cli_handler::print_version()
|
|
{
|
|
ostr_ << "clang-uml " << clanguml::version::CLANG_UML_VERSION << std::endl;
|
|
ostr_ << "Copyright (C) 2021-2023 Bartek Kryza <bkryza@gmail.com>"
|
|
<< std::endl;
|
|
ostr_ << util::get_os_name() << std::endl;
|
|
ostr_ << "Built against LLVM/Clang libraries version: "
|
|
<< LLVM_VERSION_STRING << std::endl;
|
|
ostr_ << "Using LLVM/Clang libraries version: "
|
|
<< clang::getClangFullVersion() << std::endl;
|
|
|
|
return cli_flow_t::kExit;
|
|
}
|
|
|
|
bool cli_handler::ensure_output_directory_exists(const std::string &dir)
|
|
{
|
|
namespace fs = std::filesystem;
|
|
using std::cout;
|
|
|
|
fs::path output_dir{dir};
|
|
|
|
if (fs::exists(output_dir) && !fs::is_directory(output_dir)) {
|
|
cout << "ERROR: " << dir << " is not a directory...\n";
|
|
return false;
|
|
}
|
|
|
|
if (!fs::exists(output_dir)) {
|
|
return fs::create_directories(output_dir);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
cli_flow_t cli_handler::print_diagrams_list()
|
|
{
|
|
using std::cout;
|
|
|
|
ostr_ << "The following diagrams are defined in the config file:\n";
|
|
for (const auto &[name, diagram] : config.diagrams) {
|
|
ostr_ << " - " << name << " [" << to_string(diagram->type()) << "]";
|
|
ostr_ << '\n';
|
|
}
|
|
|
|
return cli_flow_t::kExit;
|
|
}
|
|
|
|
cli_flow_t cli_handler::print_diagram_templates()
|
|
{
|
|
using std::cout;
|
|
|
|
if (!config.diagram_templates) {
|
|
ostr_ << "No diagram templates are defined in the config file\n";
|
|
return cli_flow_t::kExit;
|
|
}
|
|
|
|
ostr_ << "The following diagram templates are available:\n";
|
|
for (const auto &[name, diagram_template] : config.diagram_templates()) {
|
|
ostr_ << " - " << name << " [" << to_string(diagram_template.type)
|
|
<< "]";
|
|
if (!diagram_template.description.empty())
|
|
ostr_ << ": " << diagram_template.description;
|
|
ostr_ << '\n';
|
|
}
|
|
|
|
return cli_flow_t::kExit;
|
|
}
|
|
|
|
cli_flow_t cli_handler::print_diagram_template(const std::string &template_name)
|
|
{
|
|
if (!config.diagram_templates ||
|
|
config.diagram_templates().count(template_name) == 0) {
|
|
ostr_ << "No such diagram template: " << template_name << "\n";
|
|
return cli_flow_t::kError;
|
|
}
|
|
|
|
for (const auto &[name, diagram_template] : config.diagram_templates()) {
|
|
if (template_name == name) {
|
|
ostr_ << diagram_template.jinja_template << "\n";
|
|
return cli_flow_t::kExit;
|
|
}
|
|
}
|
|
|
|
return cli_flow_t::kError;
|
|
}
|
|
|
|
cli_flow_t cli_handler::create_config_file()
|
|
{
|
|
namespace fs = std::filesystem;
|
|
|
|
fs::path config_file{"./.clang-uml"};
|
|
|
|
if (fs::exists(config_file)) {
|
|
ostr_ << "ERROR: .clang-uml file already exists\n";
|
|
return cli_flow_t::kError;
|
|
}
|
|
|
|
YAML::Emitter out;
|
|
out.SetIndent(2);
|
|
out << YAML::BeginMap;
|
|
out << YAML::Comment("Change to directory where compile_commands.json is");
|
|
out << YAML::Key << "compilation_database_dir" << YAML::Value << ".";
|
|
out << YAML::Newline
|
|
<< YAML::Comment("Change to directory where diagram should be written");
|
|
out << YAML::Key << "output_directory" << YAML::Value << "docs/diagrams";
|
|
out << YAML::Key << "diagrams" << YAML::Value;
|
|
out << YAML::BeginMap;
|
|
out << YAML::Key << "example_class_diagram" << YAML::Value;
|
|
out << YAML::BeginMap;
|
|
out << YAML::Key << "type" << YAML::Value << "class";
|
|
out << YAML::Key << "glob" << YAML::Value;
|
|
out << YAML::BeginSeq << "src/*.cpp" << YAML::EndSeq;
|
|
out << YAML::Key << "using_namespace" << YAML::Value;
|
|
out << YAML::BeginSeq << "myproject" << YAML::EndSeq;
|
|
out << YAML::Key << "include";
|
|
out << YAML::BeginMap;
|
|
out << YAML::Key << "namespaces";
|
|
out << YAML::BeginSeq << "myproject" << YAML::EndSeq;
|
|
out << YAML::EndMap;
|
|
out << YAML::Key << "exclude";
|
|
out << YAML::BeginMap;
|
|
out << YAML::Key << "namespaces";
|
|
out << YAML::BeginSeq << "myproject::detail" << YAML::EndSeq;
|
|
out << YAML::EndMap;
|
|
out << YAML::EndMap;
|
|
out << YAML::EndMap;
|
|
out << YAML::EndMap;
|
|
out << YAML::Newline;
|
|
|
|
std::ofstream ofs(config_file);
|
|
ofs << out.c_str();
|
|
ofs.close();
|
|
|
|
return cli_flow_t::kExit;
|
|
}
|
|
|
|
cli_flow_t cli_handler::add_config_diagram(
|
|
clanguml::common::model::diagram_t type,
|
|
const std::string &config_file_path, const std::string &name)
|
|
{
|
|
namespace fs = std::filesystem;
|
|
|
|
fs::path config_file{config_file_path};
|
|
|
|
if (!fs::exists(config_file)) {
|
|
std::cerr << "ERROR: " << config_file_path << " file doesn't exists\n";
|
|
return cli_flow_t::kError;
|
|
}
|
|
|
|
YAML::Node doc = YAML::LoadFile(config_file.string());
|
|
|
|
for (YAML::const_iterator it = doc["diagrams"].begin();
|
|
it != doc["diagrams"].end(); ++it) {
|
|
if (it->first.as<std::string>() == name) {
|
|
std::cerr << "ERROR: " << config_file_path
|
|
<< " file already contains '" << name << "' diagram";
|
|
return cli_flow_t::kError;
|
|
}
|
|
}
|
|
|
|
if (type == clanguml::common::model::diagram_t::kClass) {
|
|
doc["diagrams"][name]["type"] = "class";
|
|
doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
|
|
doc["diagrams"][name]["using_namespace"] =
|
|
std::vector<std::string>{{"myproject"}};
|
|
doc["diagrams"][name]["include"]["namespaces"] =
|
|
std::vector<std::string>{{"myproject"}};
|
|
doc["diagrams"][name]["exclude"]["namespaces"] =
|
|
std::vector<std::string>{{"myproject::detail"}};
|
|
}
|
|
else if (type == clanguml::common::model::diagram_t::kSequence) {
|
|
doc["diagrams"][name]["type"] = "sequence";
|
|
doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
|
|
doc["diagrams"][name]["combine_free_functions_into_file_participants"] =
|
|
true;
|
|
doc["diagrams"][name]["using_namespace"] =
|
|
std::vector<std::string>{{"myproject"}};
|
|
doc["diagrams"][name]["include"]["paths"] =
|
|
std::vector<std::string>{{"src"}};
|
|
doc["diagrams"][name]["exclude"]["namespaces"] =
|
|
std::vector<std::string>{{"myproject::detail"}};
|
|
doc["diagrams"][name]["start_from"] =
|
|
std::vector<std::map<std::string, std::string>>{
|
|
{{"function", "main(int,const char **)"}}};
|
|
}
|
|
else if (type == clanguml::common::model::diagram_t::kPackage) {
|
|
doc["diagrams"][name]["type"] = "package";
|
|
doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
|
|
doc["diagrams"][name]["using_namespace"] =
|
|
std::vector<std::string>{{"myproject"}};
|
|
doc["diagrams"][name]["include"]["namespaces"] =
|
|
std::vector<std::string>{{"myproject"}};
|
|
doc["diagrams"][name]["exclude"]["namespaces"] =
|
|
std::vector<std::string>{{"myproject::detail"}};
|
|
}
|
|
else if (type == clanguml::common::model::diagram_t::kInclude) {
|
|
doc["diagrams"][name]["type"] = "include";
|
|
doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
|
|
doc["diagrams"][name]["relative_to"] = ".";
|
|
doc["diagrams"][name]["include"]["paths"] =
|
|
std::vector<std::string>{{"src"}};
|
|
}
|
|
|
|
YAML::Emitter out;
|
|
out.SetIndent(2);
|
|
|
|
out << doc;
|
|
out << YAML::Newline;
|
|
|
|
std::ofstream ofs(config_file);
|
|
ofs << out.c_str();
|
|
ofs.close();
|
|
|
|
return cli_flow_t::kExit;
|
|
}
|
|
|
|
cli_flow_t cli_handler::add_config_diagram_from_template(
|
|
const std::string &config_file_path, const std::string &template_name,
|
|
const std::vector<std::string> &template_variables)
|
|
{
|
|
if (!config.diagram_templates ||
|
|
!(config.diagram_templates().find(template_name) !=
|
|
config.diagram_templates().end())) {
|
|
std::cerr << "ERROR: No such diagram template: " << template_name
|
|
<< "\n";
|
|
return cli_flow_t::kError;
|
|
}
|
|
|
|
// First, try to render the template using inja and create a YAML node from
|
|
// it
|
|
inja::json ctx;
|
|
for (const auto &tv : template_variables) {
|
|
const auto var = util::split(tv, "=");
|
|
if (var.size() != 2) {
|
|
std::cerr << "ERROR: Invalid template variable " << tv << "\n";
|
|
return cli_flow_t::kError;
|
|
}
|
|
|
|
ctx[var.at(0)] = var.at(1);
|
|
}
|
|
|
|
auto diagram_template_str =
|
|
config.diagram_templates().at(template_name).jinja_template;
|
|
|
|
YAML::Node diagram_node;
|
|
|
|
try {
|
|
auto diagram_str = inja::render(diagram_template_str, ctx);
|
|
diagram_node = YAML::Load(diagram_str);
|
|
}
|
|
catch (inja::InjaError &e) {
|
|
std::cerr << "ERROR: Failed to generate diagram template '"
|
|
<< template_name << "': " << e.what() << "\n";
|
|
return cli_flow_t::kError;
|
|
}
|
|
catch (YAML::Exception &e) {
|
|
std::cerr << "ERROR: Rendering diagram template '" << template_name
|
|
<< "' resulted in invalid YAML: " << e.what() << "\n";
|
|
return cli_flow_t::kError;
|
|
}
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
fs::path config_file{config_file_path};
|
|
|
|
if (!fs::exists(config_file)) {
|
|
std::cerr << "ERROR: " << config_file_path << " file doesn't exists\n";
|
|
return cli_flow_t::kError;
|
|
}
|
|
|
|
YAML::Node doc = YAML::LoadFile(config_file.string());
|
|
|
|
const auto diagram_name = diagram_node.begin()->first.as<std::string>();
|
|
doc["diagrams"][diagram_name] = diagram_node.begin()->second;
|
|
|
|
YAML::Emitter out;
|
|
out.SetIndent(2);
|
|
|
|
out << doc;
|
|
out << YAML::Newline;
|
|
|
|
std::ofstream ofs(config_file);
|
|
ofs << out.c_str();
|
|
ofs.close();
|
|
|
|
return cli_flow_t::kExit;
|
|
}
|
|
|
|
cli_flow_t cli_handler::print_config()
|
|
{
|
|
YAML::Emitter out;
|
|
out.SetIndent(2);
|
|
|
|
out << config;
|
|
out << YAML::Newline;
|
|
|
|
ostr_ << out.c_str();
|
|
|
|
return cli_flow_t::kExit;
|
|
}
|
|
} // namespace clanguml::cli
|