Line data Source code
1 : /*!
2 : * \file esys/repo/cli/appbase.cpp
3 : * \brief
4 : *
5 : * \cond
6 : * __legal_b__
7 : *
8 : * Copyright (c) 2020-2023 Michel Gillet
9 : * Distributed under the MIT License.
10 : * (See accompanying file LICENSE.txt or
11 : * copy at https://opensource.org/licenses/MIT)
12 : *
13 : * __legal_e__
14 : * \endcond
15 : *
16 : */
17 :
18 : #include "esys/repo/esysrepo_prec.h"
19 : #include "esys/repo/cli/appbase.h"
20 : #include "esys/repo/version.h"
21 : #include "esys/repo/cli/cmd.h"
22 : #include "esys/repo/libgit2/git.h"
23 :
24 : #include <esys/trace/call.h>
25 : #include <esys/trace/macros.h>
26 :
27 : #include <boost/program_options/cmdline.hpp>
28 : #include <boost/program_options/options_description.hpp>
29 : #include <boost/program_options/parsers.hpp>
30 : #include <boost/filesystem.hpp>
31 : #include <boost/tokenizer.hpp>
32 :
33 : #ifndef WIN32
34 : #include <boost/process/search_path.hpp>
35 : #endif
36 :
37 : #ifdef WIN32
38 : #include <stdio.h>
39 :
40 : #ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
41 : #define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
42 : #endif
43 :
44 : #endif
45 :
46 : #include <iostream>
47 : #include <iomanip>
48 : #include <cassert>
49 :
50 : namespace esys::repo::cli
51 : {
52 :
53 17 : AppBase::AppBase()
54 : {
55 17 : esys::trace::Call::set_differentiate_thread(true);
56 :
57 17 : m_user_folder = std::make_shared<UserFolder>();
58 17 : ETRC_SET_THREAD_NAME("AppBase1");
59 17 : }
60 :
61 101 : AppBase::~AppBase() = default;
62 :
63 0 : void AppBase::set_os(std::ostream &os)
64 : {
65 0 : m_os = &os;
66 0 : }
67 :
68 16 : std::ostream *AppBase::get_os()
69 : {
70 16 : return m_os;
71 : }
72 :
73 161 : void AppBase::register_cmd(Cmd *cmd)
74 : {
75 161 : assert(find_cmd(cmd->get_name()) == nullptr);
76 :
77 161 : m_cmds.push_back(cmd);
78 161 : m_map_cmds[cmd->get_name()] = cmd;
79 161 : }
80 :
81 4 : std::vector<Cmd *> &AppBase::get_cmds()
82 : {
83 4 : return m_cmds;
84 : }
85 :
86 0 : const std::vector<Cmd *> &AppBase::get_cmds() const
87 : {
88 0 : return m_cmds;
89 : }
90 :
91 35 : Cmd *AppBase::find_cmd(const std::string &name)
92 : {
93 35 : auto it = m_map_cmds.find(name);
94 35 : if (it == m_map_cmds.end()) return nullptr;
95 35 : return it->second;
96 : }
97 :
98 21 : void AppBase::set_args(const std::vector<std::string> &args)
99 : {
100 21 : m_args = args;
101 21 : }
102 :
103 17 : const std::vector<std::string> &AppBase::get_args() const
104 : {
105 17 : return m_args;
106 : }
107 :
108 0 : void AppBase::set_args(int argc, char **argv)
109 : {
110 0 : std::vector<std::string> args;
111 :
112 0 : m_argc = argc;
113 :
114 : #ifdef WIN32
115 : m_executable = argv[0];
116 : m_executable = boost::filesystem::absolute(m_executable).lexically_normal().make_preferred();
117 : #else
118 0 : boost::filesystem::path temp_path = argv[0];
119 0 : m_executable = boost::process::search_path(temp_path.filename());
120 : #endif
121 :
122 0 : for (int i = 1; i < argc; ++i)
123 : {
124 : #ifdef WIN32
125 : std::vector<std::string> temp_args = po::split_winmain(argv[i]);
126 : for (auto &temp_arg : temp_args) args.push_back(temp_arg);
127 : #else
128 0 : std::string temp = argv[i];
129 0 : args.push_back(temp);
130 : #endif
131 0 : }
132 :
133 0 : set_args(args);
134 0 : }
135 :
136 21 : int AppBase::parse_and_configure()
137 : {
138 21 : auto rresult = get_user_folder()->populate_all_pathes();
139 21 : if (rresult.error())
140 : {
141 0 : warn("Couldn't populate the paths of the user folder");
142 : }
143 :
144 21 : int result = parse_args();
145 21 : if (result < 0)
146 : {
147 0 : error("command line parsing error.");
148 0 : print_help(std::cout);
149 : return result;
150 : }
151 :
152 21 : if (get_cmd().empty())
153 : {
154 5 : if (is_help())
155 : {
156 0 : print_help(std::cout);
157 : return 1;
158 : }
159 5 : if (is_doc())
160 : {
161 0 : print_doc(std::cout);
162 : return 1;
163 : }
164 5 : if (is_version())
165 : {
166 : // Same as "esysrepo version" — common CLI convention.
167 2 : m_vm.insert(std::make_pair("command", po::variable_value(std::string("version"), false)));
168 : }
169 : else
170 : {
171 4 : print_help(std::cout);
172 : return -1;
173 : }
174 : }
175 :
176 17 : auto cmd_obj = find_cmd(get_cmd());
177 17 : if (cmd_obj == nullptr)
178 : {
179 0 : error("unknown command " + get_cmd() + ".");
180 0 : print_help(std::cout);
181 : return -1;
182 : }
183 :
184 17 : cmd_obj->set_logger_if(get_logger_if());
185 17 : {
186 17 : std::vector<std::string> args = get_args();
187 : // Cmd parsers are strict and do not know --version; map it to the command name.
188 34 : if (get_cmd() == "version")
189 : {
190 3 : for (auto &arg : args)
191 : {
192 2 : if (arg == "--version")
193 : {
194 1 : arg = "version";
195 : break;
196 : }
197 : }
198 : }
199 17 : cmd_obj->set_args(args);
200 17 : }
201 17 : cmd_obj->set_user_folder(get_user_folder());
202 :
203 17 : if (is_doc())
204 : {
205 0 : cmd_obj->print_doc(std::cout);
206 : return 1;
207 : }
208 :
209 17 : if (is_help())
210 : {
211 0 : cmd_obj->print_help(std::cout);
212 : return 1;
213 : }
214 :
215 17 : return cmd_obj->parse_and_configure();
216 21 : }
217 :
218 21 : int AppBase::parse_args()
219 : {
220 21 : po::positional_options_description p;
221 21 : p.add("command", 1).add("subargs", -1);
222 :
223 21 : if (m_common_desc == nullptr)
224 : {
225 17 : m_common_desc = std::make_shared<po::options_description>("Options");
226 : // clang-format off
227 17 : m_common_desc->add_options()
228 17 : ("help,h", "Print help message")
229 17 : ("doc,d", "Print documentation")
230 17 : ("version", "Print version information (same as 'version' command)")
231 17 : ("command", po::value< std::string>(), "Command to execute")
232 17 : ("subargs", po::value<std::vector<std::string>>(), "Arguments for command")
233 17 : ("time", po::value<bool>()->default_value(false)->implicit_value(true), "Display timestamp")
234 17 : ("delta-time", po::value<bool>()->default_value(false)->implicit_value(true), "Display delta timestamp")
235 17 : ("public", po::value<bool>()->default_value(false)->implicit_value(true), "Only use https when possible")
236 17 : ("log", po::value<std::vector<std::string>>(), "Log execution to a file")
237 17 : ("verbose", po::value<int>()->default_value(0)->implicit_value(1), "Set verbose level")
238 34 : ("debug", po::value<bool>()->default_value(false)->implicit_value(true), "Set debug mode")
239 17 : ("workspace,w", po::value<std::string>(), "the path to the workspade to use")
240 17 : ("folder,f", po::value<std::string>(), "the folder to work with")
241 17 : ("trace", "enable execution trace to a file and to the console")
242 17 : ("trace-file", "enable execution trace to a file")
243 17 : ("trace-cons", "enable execution trace to the console")
244 : ;
245 : // clang-format on
246 : }
247 :
248 21 : int result = parse(m_args, *m_common_desc.get(), p, m_vm, false);
249 :
250 21 : setup_console_and_logs();
251 21 : return result;
252 21 : }
253 :
254 56 : std::string AppBase::get_cmd()
255 : {
256 214 : if (m_vm.count("command")) return m_vm["command"].as<std::string>();
257 5 : return "";
258 : }
259 :
260 21 : bool AppBase::get_debug() const
261 : {
262 63 : if (m_vm.count("debug")) return m_vm["debug"].as<bool>();
263 : return false;
264 : }
265 :
266 21 : bool AppBase::is_trace_to_file() const
267 : {
268 42 : if (m_vm.count("trace")) return true;
269 42 : if (m_vm.count("trace-file")) return true;
270 : return false;
271 : }
272 :
273 21 : bool AppBase::is_trace_to_console() const
274 : {
275 42 : if (m_vm.count("trace")) return true;
276 42 : if (m_vm.count("trace-cons")) return true;
277 : return false;
278 : }
279 :
280 22 : bool AppBase::is_help() const
281 : {
282 44 : if (m_vm.count("help")) return true;
283 : return false;
284 : }
285 :
286 22 : bool AppBase::is_doc() const
287 : {
288 44 : if (m_vm.count("doc")) return true;
289 : return false;
290 : }
291 :
292 5 : bool AppBase::is_version() const
293 : {
294 10 : return m_vm.count("version") > 0;
295 : }
296 :
297 21 : int AppBase::setup_console_and_logs()
298 : {
299 21 : bool debug = get_debug();
300 21 : bool trace_cons = is_trace_to_console();
301 21 : bool trace_file = is_trace_to_file();
302 :
303 21 : if (m_logger_mngr == nullptr) m_logger_mngr = esys::log::Mngr::get();
304 :
305 21 : std::vector<std::string> plugin_search_folders;
306 :
307 21 : int result = esys::log::Mngr::get()->find_plugin_folders(plugin_search_folders);
308 21 : if (result < 0) std::cout << "ERROR: can't find ESysLog plugin folder." << std::endl;
309 21 : if (debug)
310 : {
311 0 : std::cout << "plugin_search_folder = " << std::endl;
312 0 : for (auto idx = 0; idx < plugin_search_folders.size(); ++idx)
313 : {
314 0 : std::cout << "[" << idx << "] " << plugin_search_folders[idx] << std::endl;
315 : }
316 : }
317 :
318 : // m_logger_mngr->set_search_folder(plugin_search_folder.make_preferred().string());
319 21 : m_logger = m_logger_mngr->new_logger(esys::log::LoggerType::SPDLOG, "ESysRepo");
320 21 : if (m_logger != nullptr)
321 : {
322 42 : set_logger_if(m_logger);
323 :
324 21 : if (debug && !trace_cons && !trace_file)
325 : {
326 0 : m_logger->add_console("[%^%l%$] %v", esys::log::Level::DEBUG);
327 0 : if (!get_log_file_path().empty()) m_logger->add_basic_file(get_log_file_path());
328 0 : m_logger->set_log_level(esys::log::Level::DEBUG);
329 0 : m_logger->set_debug_level(DFT_LOGGER_DEBUG_LEVEL);
330 0 : m_logger->set_flush_log_level(esys::log::Level::DEBUG);
331 : }
332 21 : else if (trace_cons || trace_file)
333 : {
334 0 : if (trace_cons)
335 0 : m_logger->add_console("[%^%l%$] %v", esys::log::Level::TRACE);
336 0 : else if (debug)
337 0 : m_logger->add_console("[%^%l%$] %v", esys::log::Level::DEBUG);
338 : else
339 0 : m_logger->add_console("[%^%l%$] %v", esys::log::Level::INFO);
340 0 : if (!get_log_file_path().empty() && !trace_file) m_logger->add_basic_file(get_log_file_path());
341 0 : m_logger->set_log_level(esys::log::Level::TRACE);
342 0 : m_logger->set_debug_level(DFT_LOGGER_DEBUG_LEVEL);
343 0 : m_logger->set_flush_log_level(esys::log::Level::TRACE);
344 : }
345 : else
346 : {
347 21 : m_logger->add_console("[%^%l%$] %v", esys::log::Level::INFO);
348 21 : if (!get_log_file_path().empty())
349 21 : m_logger->add_basic_file(get_log_file_path(), false, esys::log::Level::INFO);
350 21 : m_logger->set_log_level(esys::log::Level::INFO);
351 21 : m_logger->set_flush_log_level(esys::log::Level::INFO);
352 : }
353 21 : if (trace_file)
354 : {
355 0 : boost::filesystem::path traces_folder;
356 :
357 0 : if ((get_user_folder() != nullptr) && (get_user_folder()->get_user_config() != nullptr))
358 0 : traces_folder = get_user_folder()->get_user_config()->get_traces_path();
359 : else
360 0 : traces_folder = boost::filesystem::current_path();
361 0 : if (!boost::filesystem::exists(traces_folder))
362 : {
363 0 : boost::filesystem::create_directories(traces_folder);
364 : }
365 0 : boost::filesystem::path traces_file = traces_folder / "file.txt";
366 0 : m_logger->add_rotating_file(traces_file.string(), 100, 10, esys::log::Level::TRACE);
367 0 : }
368 21 : if (trace_file || trace_cons)
369 : {
370 0 : m_trace = std::make_shared<esys::log::Trace>();
371 0 : m_trace->set_logger(m_logger);
372 :
373 0 : m_trace_logger = std::make_shared<esys::trace::Logger>();
374 0 : m_trace_logger->set_log_if(m_trace);
375 0 : esys::trace::Call::set_default_logger(m_trace_logger.get());
376 0 : esys::trace::Call::enable();
377 : }
378 : }
379 21 : return 0;
380 21 : }
381 :
382 0 : void AppBase::setup_terminal()
383 : {
384 : #ifdef WIN32
385 : // Enable VT sequences when attached to a real console. Piped/redirected
386 : // stdout (Cursor agent, CI, `> file`) is not an error — just skip quietly.
387 : HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
388 : if (hOut == INVALID_HANDLE_VALUE || hOut == nullptr) return;
389 :
390 : DWORD dwMode = 0;
391 : if (!GetConsoleMode(hOut, &dwMode)) return;
392 :
393 : dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
394 : SetConsoleMode(hOut, dwMode);
395 : #endif
396 0 : }
397 :
398 16 : int AppBase::run()
399 : {
400 32 : if (m_vm.count("command"))
401 : {
402 32 : std::string cmd = m_vm["command"].as<std::string>();
403 :
404 16 : auto cmd_obj = find_cmd(cmd);
405 16 : if (cmd_obj != nullptr)
406 : {
407 16 : m_git = std::make_shared<esys::repo::libgit2::Git>();
408 16 : m_vm.clear();
409 16 : cmd_obj->set_logger_if(get_logger_if());
410 16 : cmd_obj->set_console_os(get_os());
411 16 : return cmd_obj->run();
412 : }
413 : else
414 : {
415 0 : error("Comamnd '" + cmd + "' is not known.");
416 0 : return -1;
417 : }
418 16 : }
419 :
420 0 : error("A comamnd must be provided.");
421 0 : return -1;
422 : }
423 :
424 0 : void AppBase::set_error_msg(const std::string &error_msg)
425 : {
426 0 : m_error_msg = error_msg;
427 0 : }
428 :
429 0 : const std::string &AppBase::get_error_msg()
430 : {
431 0 : return m_error_msg;
432 : }
433 :
434 : struct CmpCmdName
435 : {
436 54 : bool operator()(const Cmd *a, const Cmd *b) const
437 : {
438 54 : return a->get_name() < b->get_name();
439 : }
440 : };
441 :
442 4 : void AppBase::print_help(std::ostream &os)
443 : {
444 4 : std::ostringstream oss;
445 :
446 4 : oss << "Usage: esysrepo [cmd] [cmd_options] [common_options]" << std::endl << std::endl;
447 :
448 4 : oss << "Commands:" << std::endl;
449 :
450 4 : std::vector<Cmd *> cmds = get_cmds();
451 4 : CmpCmdName cmp_cmd_name;
452 :
453 4 : std::sort(cmds.begin(), cmds.end(), cmp_cmd_name);
454 :
455 35 : for (auto cmd : cmds)
456 : {
457 31 : oss << " " << std::setw(15) << std::left << cmd->get_name() << cmd->get_description() << std::endl;
458 : }
459 4 : oss << std::endl;
460 :
461 4 : oss << "'esysrepo [cmd] --help' to get information about a cmd." << std::endl;
462 4 : oss << "'esysrepo [cmd] --doc' to get documentation about a cmd." << std::endl;
463 4 : oss << std::endl;
464 :
465 4 : if (m_common_desc == nullptr) return;
466 4 : oss << *m_common_desc;
467 :
468 4 : os << oss.str();
469 4 : }
470 :
471 0 : void AppBase::print_doc(std::ostream &os)
472 : {
473 0 : }
474 :
475 0 : std::string AppBase::get_input_filename()
476 : {
477 0 : std::string result = m_vm["input"].as<std::string>();
478 :
479 0 : return result;
480 : }
481 :
482 0 : std::string AppBase::get_output_filename()
483 : {
484 0 : std::string result;
485 :
486 0 : if (m_vm.count("output") != 0) return m_vm["output"].as<std::string>();
487 :
488 0 : boost::filesystem::path filename = get_input_filename();
489 0 : boost::filesystem::path output = filename.parent_path() / (filename.stem().string() + ".md");
490 0 : return output.make_preferred().string();
491 0 : }
492 :
493 21 : int AppBase::parse(const std::vector<std::string> &args, po::options_description &desc,
494 : po::positional_options_description &p, po::variables_map &vm, bool strict)
495 : {
496 21 : bool parse_error = false;
497 :
498 21 : m_to_parse_further.clear();
499 21 : m_vm.clear();
500 :
501 21 : try
502 : {
503 21 : if (strict == false)
504 : {
505 21 : po::parsed_options parsed =
506 42 : po::command_line_parser(args).options(desc).positional(p).allow_unregistered().run();
507 21 : m_to_parse_further = po::collect_unrecognized(parsed.options, po::include_positional);
508 21 : po::store(parsed, vm);
509 21 : }
510 : else
511 : {
512 0 : po::parsed_options parsed = po::command_line_parser(args).options(desc).positional(p).run();
513 0 : m_to_parse_further = po::collect_unrecognized(parsed.options, po::include_positional);
514 0 : po::store(parsed, vm);
515 0 : }
516 21 : po::notify(m_vm);
517 : }
518 0 : catch (po::error &e)
519 : {
520 0 : parse_error = true;
521 0 : set_error_msg(e.what());
522 0 : std::cout << e.what() << std::endl << std::flush;
523 0 : }
524 :
525 0 : if (parse_error) return -1;
526 : return 0;
527 : }
528 :
529 : /*AppBase::CmdFctType AppBase::find_cmd_fct(const std::string &cmd)
530 : {
531 : auto it = m_map_commands.find(cmd);
532 :
533 : if (it == m_map_commands.end()) return nullptr;
534 :
535 : return it->second;
536 : } */
537 :
538 42 : const std::string &AppBase::get_log_file_path()
539 : {
540 42 : m_log_file_path = "log.txt";
541 42 : return m_log_file_path;
542 : }
543 :
544 0 : void AppBase::set_logger_mngr(std::shared_ptr<esys::log::Mngr> logger_mngr)
545 : {
546 0 : m_logger_mngr = logger_mngr;
547 0 : }
548 :
549 0 : std::string AppBase::get_string(const std::string &name)
550 : {
551 0 : if (m_vm.count(name)) return m_vm[name].as<std::string>();
552 0 : return "";
553 : }
554 :
555 0 : void AppBase::set_version(const std::string &version)
556 : {
557 0 : m_version = version;
558 0 : }
559 :
560 0 : const std::string &AppBase::get_version() const
561 : {
562 0 : return m_version;
563 : }
564 :
565 0 : std::shared_ptr<po::options_description> AppBase::get_common_desc()
566 : {
567 0 : return m_common_desc;
568 : }
569 :
570 0 : int AppBase::groups_str_to_groups(const std::string &groups_str, std::vector<std::string> &groups)
571 : {
572 0 : typedef boost::tokenizer<boost::char_separator<char>> tokenizer;
573 0 : boost::char_separator<char> sep(", ");
574 0 : tokenizer tokens(groups_str, sep);
575 :
576 0 : groups.clear();
577 :
578 0 : for (tokenizer::iterator it = tokens.begin(); it != tokens.end(); ++it) groups.push_back(*it);
579 :
580 0 : return 0;
581 0 : }
582 :
583 0 : std::string AppBase::get_workspace_folder()
584 : {
585 0 : return get_string("workspace");
586 : }
587 :
588 0 : std::string AppBase::get_folder()
589 : {
590 0 : return get_string("folder");
591 : }
592 :
593 38 : std::shared_ptr<UserFolder> AppBase::get_user_folder()
594 : {
595 38 : return m_user_folder;
596 : }
597 :
598 0 : std::vector<std::string> AppBase::get_sub_args()
599 : {
600 0 : if (m_vm.count("subargs") == 0) return std::vector<std::string>();
601 :
602 0 : return m_vm["subargs"].as<std::vector<std::string>>();
603 : }
604 :
605 : } // namespace esys::repo::cli
|