0.6.0
C++ to UML diagram generator based on Clang
Loading...
Searching...
No Matches
util.cc
Go to the documentation of this file.
1/**
2 * @file src/util/util.cc
3 *
4 * Copyright (c) 2021-2025 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 "util.h"
19
20#include <spdlog/spdlog.h>
21
22#include <regex>
23#if __has_include(<sys/utsname.h>)
24#include <sys/utsname.h>
25#endif
26
27namespace clanguml::util {
28
29static const auto WHITESPACE = " \n\r\t\f\v";
30
31namespace {
32class pipe_t {
33public:
34 explicit pipe_t(const std::string &command, int &result)
35 : result_{result}
36 ,
37#if defined(__linux) || defined(__unix) || defined(__APPLE__)
38 pipe_{popen(fmt::format("{} 2>&1", command).c_str(), "r")}
39#elif defined(_WIN32)
40 pipe_{_popen(command.c_str(), "r")}
41#endif
42 {
43 }
44
45 ~pipe_t() { reset(); }
46
47 operator bool() const { return pipe_ != nullptr; }
48
49 FILE *get() const { return pipe_; }
50
51 void reset()
52 {
53 if (pipe_ == nullptr)
54 return;
55
56#if defined(__linux) || defined(__unix) || defined(__APPLE__)
57 result_ = pclose(pipe_);
58#elif defined(_WIN32)
59 result_ = _pclose(pipe_);
60#endif
61 pipe_ = nullptr;
62 }
63
64private:
65 int &result_;
66 FILE *pipe_;
67};
68} // namespace
69
70std::string get_process_output(const std::string &command)
71{
72 constexpr size_t kBufferSize{1024};
73 std::array<char, kBufferSize> buffer{};
74 std::string output;
75 int result{EXIT_FAILURE};
76
77 pipe_t pipe{command, result};
78
79 if (!pipe) {
80 throw std::runtime_error("popen() failed!");
81 }
82
83 while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
84 output += buffer.data();
85 }
86
87 pipe.reset();
88
89 if (result != EXIT_SUCCESS) {
90 throw std::runtime_error(
91 fmt::format("External command '{}' failed: {}", command, output));
92 }
93
94 return output;
95}
96
97void check_process_output(const std::string &command)
98{
99 constexpr size_t kBufferSize{1024};
100 std::array<char, kBufferSize> buffer{};
101 int result{EXIT_FAILURE};
102 std::string output;
103 pipe_t pipe{command, result};
104
105 if (!pipe) {
106 throw std::runtime_error("popen() failed!");
107 }
108
109 while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
110 output += buffer.data();
111 }
112
113 pipe.reset();
114
115 if (result != EXIT_SUCCESS) {
116 throw std::runtime_error(
117 fmt::format("External command '{}' failed: {}", command, output));
118 }
119}
120
121std::string get_env(const std::string &name)
122{
123#if defined(__linux) || defined(__unix)
124 const char *value = std::getenv(name.c_str()); // NOLINT
125
126 if (value == nullptr)
127 return {};
128
129 return std::string{value};
130#elif defined(WINDOWS) || defined(_WIN32) || defined(WIN32)
131 static constexpr auto kMaxEnvLength = 2096U;
132 static char value[kMaxEnvLength];
133 const DWORD ret =
134 GetEnvironmentVariableA(name.c_str(), value, kMaxEnvLength);
135 if (ret == 0 || ret > kMaxEnvLength)
136 return {};
137 else
138 return value;
139#else
140 return {};
141#endif
142}
143
145{
146 const auto env = get_env("CLANGUML_GIT_COMMIT");
147
148 if (!env.empty())
149 return true;
150
151 std::string output;
152
153 try {
154 output = get_process_output("git rev-parse --git-dir");
155 }
156 catch (std::runtime_error &e) {
157 return false;
158 }
159
160 return contains(trim(output), ".git");
161}
162
163std::string run_git_command(
164 const std::string &cmd, const std::string &env_override)
165{
166 auto env = get_env(env_override);
167
168 if (!env.empty())
169 return env;
170
171 std::string output;
172
173 try {
174 output = get_process_output(cmd);
175 }
176 catch (std::runtime_error &e) {
177 return {};
178 }
179
180 return trim(output);
181}
182
183std::string get_git_branch()
184{
185 return run_git_command(
186 "git rev-parse --abbrev-ref HEAD", "CLANGUML_GIT_BRANCH");
187}
188
189std::string get_git_revision()
190{
191 return run_git_command(
192 "git describe --tags --always", "CLANGUML_GIT_REVISION");
193}
194
195std::string get_git_commit()
196{
197 return run_git_command("git rev-parse HEAD", "CLANGUML_GIT_COMMIT");
198}
199
201{
202 return run_git_command(
203 "git rev-parse --show-toplevel", "CLANGUML_GIT_TOPLEVEL_DIR");
204}
205
206std::string get_os_name()
207{
208#ifdef _WIN32
209 return "Windows, 32-bit";
210#elif _WIN64
211 return "Windows, 64-bit";
212#elif __has_include(<sys/utsname.h>)
213 struct utsname utsn; // NOLINT
214 uname(&utsn);
215 return fmt::format("{} {} {}", utsn.sysname, utsn.machine, utsn.release);
216#elif __linux__
217 return "Linux";
218#elif __APPLE__ || __MACH__
219 return "macOS";
220#elif __FreeBSD__
221 return "FreeBSD";
222#elif __unix__ || __unix
223 return "Unix";
224#else
225 return "Unknown";
226#endif
227}
228
229std::string ltrim(const std::string &s)
230{
231 const size_t start = s.find_first_not_of(WHITESPACE);
232 return (start == std::string::npos) ? "" : s.substr(start);
233}
234
235std::string rtrim(const std::string &s)
236{
237 const size_t end = s.find_last_not_of(WHITESPACE);
238 return (end == std::string::npos) ? "" : s.substr(0, end + 1);
239}
240
241std::string trim_typename(const std::string &s)
242{
243 auto res = trim(s);
244 if (res.find("typename ") == 0)
245 return res.substr(strlen("typename "));
246
247 return res;
248}
249
250std::string trim(const std::string &s) { return rtrim(ltrim(s)); }
251
252std::optional<std::pair<std::string, std::string>> split_at_first(
253 const std::string &separator, const std::string &input)
254{
255 std::optional<std::pair<std::string, std::string>> res;
256
257 std::size_t pos = input.find(separator);
258
259 if (pos == std::string::npos) {
260 return res;
261 }
262
263 auto before = input.substr(0, pos);
264 auto after = input.substr(pos + separator.size());
265
266 return std::make_optional<std::pair<std::string, std::string>>(
267 std::move(before), std::move(after));
268}
269
270std::vector<std::string> split(
271 std::string str, std::string_view delimiter, bool skip_empty)
272{
273 std::vector<std::string> result;
274
275 if (!contains(str, delimiter)) {
276 if (!str.empty())
277 result.push_back(std::move(str));
278 else if (!skip_empty)
279 result.push_back(std::move(str));
280 }
281 else
282 while (static_cast<unsigned int>(!str.empty()) != 0U) {
283 auto index = str.find(delimiter);
284 if (index != std::string::npos) {
285 auto tok = str.substr(0, index);
286 if (!tok.empty())
287 result.push_back(std::move(tok));
288 else if (!skip_empty)
289 result.push_back(std::move(tok));
290
291 str = str.substr(index + delimiter.size());
292 }
293 else {
294 if (!str.empty())
295 result.push_back(std::move(str));
296 else if (!skip_empty)
297 result.push_back(std::move(str));
298 str = "";
299 }
300 }
301
302 return result;
303}
304
305std::vector<std::string> split_isspace(std::string str)
306{
307 std::vector<std::string> result;
308
309 while (static_cast<unsigned int>(!str.empty()) != 0U) {
310 auto index = std::find_if(
311 str.begin(), str.end(), [](auto c) { return std::isspace(c); });
312 if (index != str.end()) {
313 auto tok = str.substr(0, std::distance(str.begin(), index));
314 if (!tok.empty())
315 result.push_back(std::move(tok));
316 str = str.substr(std::distance(str.begin(), index) + 1);
317 }
318 else {
319 if (!str.empty())
320 result.push_back(str);
321 str = "";
322 }
323 }
324 return result;
325}
326
327std::string join(
328 const std::vector<std::string> &toks, std::string_view delimiter)
329{
330 return fmt::format("{}", fmt::join(toks, delimiter));
331}
332
333std::string abbreviate(const std::string &s, const unsigned int max_length)
334{
335 if (s.size() <= max_length)
336 return s;
337
338 auto res = s;
339
340 res.resize(max_length);
341
342 if (res.size() > 3) {
343 res.replace(res.size() - 3, 3, 3, '.');
344 }
345
346 return res;
347}
348
350 const std::string &input, std::tuple<std::string, size_t, size_t> &result)
351{
352 const std::regex alias_regex(R"((@A\‍([^\).]+\)))");
353
354 auto alias_it =
355 std::sregex_iterator(input.begin(), input.end(), alias_regex);
356 auto end_it = std::sregex_iterator();
357
358 if (alias_it == end_it)
359 return false;
360
361 const std::smatch &match = *alias_it;
362 const std::string alias = match.str().substr(3, match.str().size() - 4);
363
364 std::get<0>(result) = alias;
365 std::get<1>(result) = match.position();
366 std::get<2>(result) = match.length();
367
368 return true;
369}
370
371bool replace_all(std::string &input, const std::string &pattern,
372 const std::string &replace_with)
373{
374 bool replaced{false};
375
376 auto pos = input.find(pattern);
377 while (pos < input.size()) {
378 input.replace(pos, pattern.size(), replace_with);
379 pos = input.find(pattern, pos + replace_with.size());
380 replaced = true;
381 }
382
383 return replaced;
384}
385
386template <> bool starts_with(const std::string &s, const std::string &prefix)
387{
388 return s.rfind(prefix, 0) == 0;
389}
390
391template <> bool ends_with(const std::string &value, const std::string &suffix)
392{
393 if (suffix.size() > value.size())
394 return false;
395 return std::equal(suffix.rbegin(), suffix.rend(), value.rbegin());
396}
397
398std::size_t hash_seed(std::size_t seed)
399{
400 constexpr auto kSeedStart{0x6a3712b5};
401 constexpr auto kSeedShiftFirst{6};
402 constexpr auto kSeedShiftSecond{2};
403
404 return kSeedStart + (seed << kSeedShiftFirst) + (seed >> kSeedShiftSecond);
405}
406
407std::string path_to_url(const std::filesystem::path &p)
408{
409 std::vector<std::string> path_tokens;
410 auto it = p.begin();
411 if (p.has_root_directory()) {
412#ifdef _MSC_VER
413 // On Windows convert the root path using its drive letter, e.g.:
414 // C:\A\B\include.h -> /c/A/B/include.h
415 if (p.root_name().string().size() > 1) {
416 if (p.is_absolute()) {
417 path_tokens.push_back(std::string{
418 std::tolower(p.root_name().string().at(0), std::locale())});
419 }
420 it++;
421 }
422#endif
423 it++;
424 }
425
426 for (; it != p.end(); it++)
427 path_tokens.push_back(it->string());
428
429 if (p.has_root_directory())
430 return fmt::format("/{}", fmt::join(path_tokens, "/"));
431
432 return fmt::format("{}", fmt::join(path_tokens, "/"));
433}
434
435std::filesystem::path ensure_path_is_absolute(
436 const std::filesystem::path &p, const std::filesystem::path &root)
437{
438 if (p.is_absolute())
439 return p;
440
441 auto result = root / p;
442 result = result.lexically_normal();
443 result.make_preferred();
444
445 return result;
446}
447
449 const std::filesystem::path &child, const std::filesystem::path &base)
450{
451 if (child.has_root_directory() != base.has_root_directory())
452 return false;
453
454 const auto child_ln = child.lexically_normal();
455 const auto base_ln = base.lexically_normal();
456
457 if (child_ln == base_ln)
458 return true;
459
460 auto rel = child_ln.lexically_relative(base_ln);
461 return !rel.empty() && rel.native()[0] != '.';
462}
463
464std::string format_message_comment(const std::string &comment, unsigned width)
465{
466 if (width == 0)
467 return comment;
468
469 std::string result;
470
471 if (comment.empty())
472 return result;
473
474 auto tokens = split_isspace(comment);
475
476 if (tokens.empty())
477 return result;
478
479 unsigned current_line_length{0};
480 for (const auto &token : tokens) {
481 if (current_line_length < width) {
482 result += token;
483 result += ' ';
484 }
485 else {
486 result.back() = '\n';
487 current_line_length = 0;
488 result += token;
489 result += ' ';
490 }
491
492 current_line_length += token.size() + 1;
493 }
494
495 result.pop_back();
496
497 return result;
498}
499
500std::optional<std::pair<std::string, std::string>> find_entry_by_path_prefix(
501 const std::map<std::string, std::string> &m, const std::string &path)
502{
503 if (m.empty())
504 return {};
505
506 // Extract keys and sort them by length in descending order
507 std::vector<std::string> keys;
508 keys.reserve(m.size());
509 for (const auto &[key, pattern] : m) {
510 keys.push_back(key);
511 }
512
513 std::sort(keys.begin(), keys.end(),
514 [](const std::string &a, const std::string &b) {
515 return a.size() > b.size();
516 });
517
518 std::filesystem::path file_path{path};
519
520 for (const auto &key : keys) {
521 const auto &pattern = m.at(key);
522 std::filesystem::path key_path{key};
523
524 if (is_relative_to(file_path, key_path)) {
525 return {{key, pattern}};
526 }
527 }
528
529 if ((path.empty() || file_path.is_relative() || path == ".") &&
530 m.count(".") > 0) {
531 return {{".", m.at(".")}};
532 }
533
534 return {};
535}
536} // namespace clanguml::util