0.5.4
C++ to UML diagram generator based on Clang
Loading...
Searching...
No Matches
cli_handler.cc
Go to the documentation of this file.
1/**
2 * @file src/options/cli_handler.cc
3 *
4 * Copyright (c) 2021-2024 Bartek Kryza <bkryza@gmail.com>
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18#include "cli_handler.h"
19
24#include "util/util.h"
25#include "version.h"
26
27#include <clang/Basic/Version.h>
28#include <clang/Config/config.h>
29#include <indicators/indicators.hpp>
30
31namespace clanguml::cli {
33 std::ostream &ostr, std::shared_ptr<spdlog::logger> logger)
34 : ostr_{ostr}
35 , logger_{std::move(logger)}
36{
37}
38
40{
41 spdlog::drop("clanguml-logger");
42
43 spdlog::register_logger(logger_);
44
45 logger_->set_pattern("[%^%l%^] [tid %t] %v");
46
47 if (verbose == 0) {
48 logger_->set_level(spdlog::level::err);
49 }
50 else if (verbose == 1) {
51 logger_->set_level(spdlog::level::info);
52 }
53 else if (verbose == 2) {
54 logger_->set_level(spdlog::level::debug);
55 }
56 else {
57 logger_->set_level(spdlog::level::trace);
58 }
59}
60
61cli_flow_t cli_handler::parse(int argc, const char **argv)
62{
63 static const std::map<std::string, clanguml::common::generator_type_t>
64 generator_type_names{
68
69 app.add_option("-c,--config", config_path,
70 "Location of configuration file, when '-' read from stdin");
71 app.add_option("-d,--compile-database", compilation_database_dir,
72 "Location of compilation database directory");
73 app.add_option("-n,--diagram-name", diagram_names,
74 "List of diagram names to generate");
75 app.add_option("-g,--generator", generators,
76 "Name of the generator (default: plantuml)")
77 ->transform(CLI::CheckedTransformer(generator_type_names));
78 app.add_option("-o,--output-directory", output_directory,
79 "Override output directory specified in config file");
80 app.add_option("-t,--thread-count", thread_count,
81 "Thread pool size (0 = hardware concurrency)");
82 app.add_flag("-V,--version", show_version, "Print version and exit");
83 app.add_flag("-v,--verbose", verbose,
84 "Verbose logging (use multiple times to increase - e.g. -vvv)");
85 app.add_flag(
86 "-p,--progress", progress, "Show progress bars for generated diagrams");
87 app.add_flag("-q,--quiet", quiet, "Minimal logging");
88 app.add_flag("-l,--list-diagrams", list_diagrams,
89 "Print list of diagrams defined in the config file");
90 app.add_flag("--init", initialize, "Initialize example config file");
91 app.add_option("--add-compile-flag", add_compile_flag,
92 "Add a compilation flag to each entry in the compilation database");
93 app.add_option("--remove-compile-flag", remove_compile_flag,
94 "Remove a compilation flag from each entry in the compilation "
95 "database");
96#if !defined(_WIN32)
97 app.add_option("--query-driver", query_driver,
98 "Query the specific compiler driver to extract system paths and add to "
99 "compile commands (e.g. arm-none-eabi-g++)");
100#endif
101 app.add_flag("--allow-empty-diagrams", allow_empty_diagrams,
102 "Do not raise an error when generated diagram model is empty");
103 app.add_option(
104 "--add-class-diagram", add_class_diagram, "Add class diagram config");
105 app.add_option("--add-sequence-diagram", add_sequence_diagram,
106 "Add sequence diagram config");
107 app.add_option("--add-package-diagram", add_package_diagram,
108 "Add package diagram config");
109 app.add_option("--add-include-diagram", add_include_diagram,
110 "Add include diagram config");
111 app.add_option("--add-diagram-from-template", add_diagram_from_template,
112 "Add diagram config based on diagram template");
113 app.add_option("--template-var", template_variables,
114 "Specify a value for a template variable");
115 app.add_flag("--list-templates", list_templates,
116 "List all available diagram templates");
117 app.add_option("--show-template", show_template,
118 "Show specific diagram template definition");
119 app.add_flag(
120 "--dump-config", dump_config, "Print effective config to stdout");
121 app.add_flag("--paths-relative-to-pwd", paths_relative_to_pwd,
122 "If true, all paths in configuration files are relative to the $PWD "
123 "instead of actual location of `.clang-uml` file.");
124 app.add_flag("--no-metadata", no_metadata,
125 "Skip metadata (e.g. clang-uml version) from diagrams");
126 app.add_flag("--print-from,--print-start-from", print_from,
127 "Print all possible 'from' values for a given diagram");
128 app.add_flag("--print-to", print_to,
129 "Print all possible 'to' values for a given diagram");
130 app.add_flag("--no-validate", no_validate,
131 "Do not perform configuration file schema validation");
132 app.add_flag("--validate-only", validate_only,
133 "Perform configuration file schema validation and exit");
134 app.add_flag("-r,--render_diagrams", render_diagrams,
135 "Automatically render generated diagrams using appropriate command");
136 app.add_option("--plantuml-cmd", plantuml_cmd,
137 "Command template to render PlantUML diagram, `{}` will be replaced "
138 "with diagram name.");
139 app.add_option("--mermaid-cmd", mermaid_cmd,
140 "Command template to render MermaidJS diagram, `{}` will be replaced "
141 "with diagram name.");
142
143 try {
144 app.parse(argc, argv);
145 }
146 catch (const CLI::CallForHelp &e) {
147 exit(app.exit(e)); // NOLINT(concurrency-mt-unsafe)
148 }
149 catch (const CLI::Success &e) {
150 return cli_flow_t::kExit;
151 }
152 catch (const CLI::ParseError &e) {
153 exit(app.exit(e)); // NOLINT(concurrency-mt-unsafe)
154 }
155
157 verbose = 0;
158 else
159 verbose++;
160
161 if (progress)
162 verbose = 0;
163
165}
166
167cli_flow_t cli_handler::handle_options(int argc, const char **argv)
168{
169 auto res = parse(argc, argv);
170
171 if (res != cli_flow_t::kContinue)
172 return res;
173
175
177
178 if (res != cli_flow_t::kContinue)
179 return res;
180
181 res = load_config();
182
183 if (res != cli_flow_t::kContinue)
184 return res;
185
187
188 config.inherit();
189
190 if (progress) {
191 spdlog::drop("clanguml-logger");
192
193 // Setup null logger for clean progress indicators
194 std::vector<spdlog::sink_ptr> sinks;
195 logger_ = std::make_shared<spdlog::logger>(
196 "clanguml-logger", begin(sinks), end(sinks));
197 spdlog::register_logger(logger_);
198 }
199
200 return res;
201}
202
204{
205 if (show_version) {
206 return print_version();
207 }
208
209 if ((config_path == "-") &&
211 add_class_diagram.has_value() || add_sequence_diagram.has_value() ||
212 add_package_diagram.has_value() ||
213 add_include_diagram.has_value())) {
214
215 LOG_ERROR(
216 "ERROR: Cannot add a diagram config to configuration from stdin");
217
218 return cli_flow_t::kError;
219 }
220
221 if (print_from || print_to) {
222 if (diagram_names.size() != 1) {
223 LOG_ERROR("ERROR: '--print-from' and '--print-to' require "
224 "specifying one diagram name using '-n' option");
225
226 return cli_flow_t::kError;
227 }
228 }
229
230 if (initialize) {
231 return create_config_file();
232 }
233
234 if (config_path != "-") {
235 if (add_class_diagram) {
236 return add_config_diagram(
239 }
240
242 return add_config_diagram(
245 }
246
248 return add_config_diagram(
251 }
252
254 return add_config_diagram(
257 }
258 }
259
261}
262
264{
265 try {
268 if (validate_only) {
269 std::cout << "Configuration file " << config_path << " is valid.\n";
270
271 return cli_flow_t::kExit;
272 }
273
275 }
276 catch (std::runtime_error &e) {
277 LOG_ERROR(e.what());
278 }
279
280 return cli_flow_t::kError;
281}
282
284{
285 if (dump_config) {
286 return print_config();
287 }
288
289 if (list_diagrams) {
290 return print_diagrams_list();
291 }
292
293 if (list_templates) {
295 }
296
297 if (show_template) {
299 }
300
304 }
305
306 LOG_INFO("Loaded clang-uml config from {}", config_path);
307
310 }
311
312 //
313 // Override selected config options from command line
314 //
318 .string());
319 }
320
322
323 // Override the output directory from the config
324 // with the value from the command line if any
327
328 if (output_directory) {
331 }
332
333 LOG_INFO("Loading compilation database from {} directory",
335
337 return cli_flow_t::kError;
338
339 //
340 // Append add_compile_flags and remove_compile_flags to the config
341 //
342 if (add_compile_flag) {
343 std::copy(add_compile_flag->begin(), add_compile_flag->end(),
344 std::back_inserter(config.add_compile_flags.value));
346 }
347
349 std::copy(remove_compile_flag->begin(), remove_compile_flag->end(),
350 std::back_inserter(config.remove_compile_flags.value));
351 config.remove_compile_flags.has_value = true;
352 }
353
354 if (plantuml_cmd) {
355 if (!config.puml)
356 config.puml.set({});
357
358 config.puml().cmd = plantuml_cmd.value();
359 }
360
361 if (mermaid_cmd) {
362 if (!config.mermaid)
363 config.mermaid.set({});
364
365 config.mermaid().cmd = mermaid_cmd.value();
366 }
367
368#if !defined(_WIN32)
369 if (query_driver) {
371 }
372#endif
373
375}
376
378{
379 runtime_config cfg;
381 cfg.verbose = verbose;
383 cfg.print_to = print_to;
384 cfg.progress = progress;
388
389 return cfg;
390}
391
392void cli_handler::set_config_path(const std::string &path)
393{
394 config_path = path;
395}
396
398{
399 ostr_ << "clang-uml " << clanguml::version::CLANG_UML_VERSION << '\n';
400 ostr_ << "Copyright (C) 2021-2024 Bartek Kryza <bkryza@gmail.com>" << '\n';
401 ostr_ << util::get_os_name() << '\n';
402 ostr_ << "Built against LLVM/Clang libraries version: "
403 << LLVM_VERSION_STRING << '\n';
404 ostr_ << "Using LLVM/Clang libraries version: "
405 << clang::getClangFullVersion() << '\n';
406
407 return cli_flow_t::kExit;
408}
409
411{
412 namespace fs = std::filesystem;
413 using std::cout;
414
415 fs::path output_dir{dir};
416
417 if (fs::exists(output_dir) && !fs::is_directory(output_dir)) {
418 cout << "ERROR: " << dir << " is not a directory...\n";
419 return false;
420 }
421
422 if (!fs::exists(output_dir)) {
423 return fs::create_directories(output_dir);
424 }
425
426 return true;
427}
428
430{
431 using std::cout;
432
433 ostr_ << "The following diagrams are defined in the config file:\n";
434 for (const auto &[name, diagram] : config.diagrams) {
435 ostr_ << " - " << name << " [" << to_string(diagram->type()) << "]";
436 ostr_ << '\n';
437 }
438
439 return cli_flow_t::kExit;
440}
441
443{
444 using std::cout;
445
447 ostr_ << "No diagram templates are defined in the config file\n";
448 return cli_flow_t::kExit;
449 }
450
451 ostr_ << "The following diagram templates are available:\n";
452 for (const auto &[name, diagram_template] : config.diagram_templates()) {
453 ostr_ << " - " << name << " [" << to_string(diagram_template.type)
454 << "]";
455 if (!diagram_template.description.empty())
456 ostr_ << ": " << diagram_template.description;
457 ostr_ << '\n';
458 }
459
460 return cli_flow_t::kExit;
461}
462
463cli_flow_t cli_handler::print_diagram_template(const std::string &template_name)
464{
466 config.diagram_templates().count(template_name) == 0) {
467 ostr_ << "No such diagram template: " << template_name << "\n";
468 return cli_flow_t::kError;
469 }
470
471 for (const auto &[name, diagram_template] : config.diagram_templates()) {
472 if (template_name == name) {
473 ostr_ << diagram_template.jinja_template << "\n";
474 return cli_flow_t::kExit;
475 }
476 }
477
478 return cli_flow_t::kError;
479}
480
482{
483 namespace fs = std::filesystem;
484
485 fs::path config_file{config_path};
486
487 if (fs::exists(config_file)) {
488 ostr_ << "ERROR: .clang-uml file already exists\n";
489 return cli_flow_t::kError;
490 }
491
492 YAML::Emitter out;
493 out.SetIndent(2);
494 out << YAML::BeginMap;
495 out << YAML::Comment("Change to directory where compile_commands.json is");
496 out << YAML::Key << "compilation_database_dir" << YAML::Value << ".";
497 out << YAML::Newline
498 << YAML::Comment("Change to directory where diagram should be written");
499 out << YAML::Key << "output_directory" << YAML::Value << "docs/diagrams";
500 out << YAML::Key << "diagrams" << YAML::Value;
501 out << YAML::BeginMap;
502 out << YAML::Key << "example_class_diagram" << YAML::Value;
503 out << YAML::BeginMap;
504 out << YAML::Key << "type" << YAML::Value << "class";
505 out << YAML::Key << "glob" << YAML::Value;
506 out << YAML::BeginSeq << "src/*.cpp" << YAML::EndSeq;
507 out << YAML::Key << "using_namespace" << YAML::Value;
508 out << YAML::BeginSeq << "myproject" << YAML::EndSeq;
509 out << YAML::Key << "include";
510 out << YAML::BeginMap;
511 out << YAML::Key << "namespaces";
512 out << YAML::BeginSeq << "myproject" << YAML::EndSeq;
513 out << YAML::EndMap;
514 out << YAML::Key << "exclude";
515 out << YAML::BeginMap;
516 out << YAML::Key << "namespaces";
517 out << YAML::BeginSeq << "myproject::detail" << YAML::EndSeq;
518 out << YAML::EndMap;
519 out << YAML::EndMap;
520 out << YAML::EndMap;
521 out << YAML::EndMap;
522 out << YAML::Newline;
523
524 std::ofstream ofs(config_file);
525 ofs << out.c_str();
526 ofs.close();
527
528 return cli_flow_t::kExit;
529}
530
533 const std::string &config_file_path, const std::string &name)
534{
535 namespace fs = std::filesystem;
536
537 fs::path config_file{config_file_path};
538
539 if (!fs::exists(config_file)) {
540 std::cerr << "ERROR: " << config_file_path << " file doesn't exists\n";
541 return cli_flow_t::kError;
542 }
543
544 YAML::Node doc = YAML::LoadFile(config_file.string());
545
546 for (YAML::const_iterator it = doc["diagrams"].begin();
547 it != doc["diagrams"].end(); ++it) {
548 if (it->first.as<std::string>() == name) {
549 std::cerr << "ERROR: " << config_file_path
550 << " file already contains '" << name << "' diagram";
551 return cli_flow_t::kError;
552 }
553 }
554
556 doc["diagrams"][name]["type"] = "class";
557 doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
558 doc["diagrams"][name]["using_namespace"] =
559 std::vector<std::string>{{"myproject"}};
560 doc["diagrams"][name]["include"]["namespaces"] =
561 std::vector<std::string>{{"myproject"}};
562 doc["diagrams"][name]["exclude"]["namespaces"] =
563 std::vector<std::string>{{"myproject::detail"}};
564 }
566 doc["diagrams"][name]["type"] = "sequence";
567 doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
568 doc["diagrams"][name]["combine_free_functions_into_file_participants"] =
569 true;
570 doc["diagrams"][name]["inline_lambda_messages"] = false;
571 doc["diagrams"][name]["generate_message_comments"] = false;
572 doc["diagrams"][name]["generate_condition_statements"] = false;
573 doc["diagrams"][name]["using_namespace"] =
574 std::vector<std::string>{{"myproject"}};
575 doc["diagrams"][name]["include"]["paths"] =
576 std::vector<std::string>{{"src"}};
577 doc["diagrams"][name]["exclude"]["namespaces"] =
578 std::vector<std::string>{{"myproject::detail"}};
579 doc["diagrams"][name]["start_from"] =
580 std::vector<std::map<std::string, std::string>>{
581 {{"function", "main(int,const char **)"}}};
582 }
584 doc["diagrams"][name]["type"] = "package";
585 doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
586 doc["diagrams"][name]["using_namespace"] =
587 std::vector<std::string>{{"myproject"}};
588 doc["diagrams"][name]["include"]["namespaces"] =
589 std::vector<std::string>{{"myproject"}};
590 doc["diagrams"][name]["exclude"]["namespaces"] =
591 std::vector<std::string>{{"myproject::detail"}};
592 }
594 doc["diagrams"][name]["type"] = "include";
595 doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
596 doc["diagrams"][name]["relative_to"] = ".";
597 doc["diagrams"][name]["include"]["paths"] =
598 std::vector<std::string>{{"src"}};
599 }
600
601 YAML::Emitter out;
602 out.SetIndent(2);
603
604 out << doc;
605 out << YAML::Newline;
606
607 std::ofstream ofs(config_file);
608 ofs << out.c_str();
609 ofs.close();
610
611 return cli_flow_t::kExit;
612}
613
615 const std::string &config_file_path, const std::string &template_name)
616{
618 !(config.diagram_templates().find(template_name) !=
619 config.diagram_templates().end())) {
620 std::cerr << "ERROR: No such diagram template: " << template_name
621 << "\n";
622 return cli_flow_t::kError;
623 }
624
625 // First, try to render the template using inja and create a YAML node
626 // from it
627 inja::json ctx;
628 for (const auto &tv : template_variables) {
629 const auto var = util::split(tv, "=");
630 if (var.size() != 2) {
631 std::cerr << "ERROR: Invalid template variable " << tv << "\n";
632 return cli_flow_t::kError;
633 }
634
635 ctx[var.at(0)] = var.at(1);
636 }
637
638 auto diagram_template_str =
639 config.diagram_templates().at(template_name).jinja_template;
640
641 YAML::Node diagram_node;
642
643 try {
644 auto diagram_str = inja::render(diagram_template_str, ctx);
645 diagram_node = YAML::Load(diagram_str);
646 }
647 catch (inja::InjaError &e) {
648 std::cerr << "ERROR: Failed to generate diagram template '"
649 << template_name << "': " << e.what() << "\n";
650 return cli_flow_t::kError;
651 }
652 catch (YAML::Exception &e) {
653 std::cerr << "ERROR: Rendering diagram template '" << template_name
654 << "' resulted in invalid YAML: " << e.what() << "\n";
655 return cli_flow_t::kError;
656 }
657
658 namespace fs = std::filesystem;
659
660 fs::path config_file{config_file_path};
661
662 if (!fs::exists(config_file)) {
663 std::cerr << "ERROR: " << config_file_path << " file doesn't exists\n";
664 return cli_flow_t::kError;
665 }
666
667 YAML::Node doc = YAML::LoadFile(config_file.string());
668
669 const auto diagram_name = diagram_node.begin()->first.as<std::string>();
670 doc["diagrams"][diagram_name] = diagram_node.begin()->second;
671
672 YAML::Emitter out;
673 out.SetIndent(2);
674
675 out << doc;
676 out << YAML::Newline;
677
678 std::ofstream ofs(config_file);
679 ofs << out.c_str();
680 ofs.close();
681
682 return cli_flow_t::kExit;
683}
684
686{
687 YAML::Emitter out;
688 out.SetIndent(2);
689
690 out << config;
691 out << YAML::Newline;
692
693 ostr_ << out.c_str();
694
695 return cli_flow_t::kExit;
696}
697} // namespace clanguml::cli