MaCh3  2.8.0
Reference Guide
MaCh3Program.cpp
Go to the documentation of this file.
1 
4 #include <dlfcn.h>
5 #include <filesystem>
6 #include <iostream>
7 #include <memory>
8 #include <sstream>
9 #include "Manager/MaCh3Logger.h"
10 #include "CLI/MaCh3Program.hpp"
11 
12 using std::vector, std::string, std::map;
13 namespace fs = std::filesystem;
14 
15 namespace M3{
16  bool MaCh3Program::write_file(const fs::path& path, std::string_view content) const{
17  try {
18  fs::create_directories(path.parent_path());
19  std::ofstream out(path);
20  if (!out) return false;
21  out << content;
22  return true;
23  } catch (...) {
24  return false;
25  }
26  }
27 
28  std::string MaCh3Program::detect_shell() const{
29  const char* shell = std::getenv("SHELL");
30  if (!shell) return "";
31 
32  std::string s(shell);
33  if (s.find("bash") != std::string::npos) return "bash";
34  if (s.find("zsh") != std::string::npos) return "zsh";
35 
36  return "";
37  }
38 
40  const char* home = std::getenv("HOME");
41  if (!home) {
42  std::cerr << "Cannot determine HOME directory\n";
43  return;
44  }
45 
46  std::string shell = detect_shell();
47  if (shell.empty()) {
48  std::cerr << "Could not detect your shell. Supported: bash, zsh\n";
49  return;
50  }
51 
52  // Resolve the absolute path of this binary so the completion script
53  // works even when mach3 is not on PATH.
54  std::string binary_path = "mach3";
55  try {
56  binary_path = fs::read_symlink("/proc/self/exe").string();
57  } catch (...) {}
58 
59  // Replace the generic "mach3" invocation in the template with the
60  // absolute binary path so the script is self-contained.
61  auto embed_path = [&](std::string_view tmpl) -> std::string {
62  std::string s(tmpl);
63  const std::string placeholder = "mach3 --complete";
64  const std::string replacement = binary_path + " --complete";
65  for (std::size_t pos = s.find(placeholder); pos != std::string::npos;
66  pos = s.find(placeholder, pos + replacement.size()))
67  s.replace(pos, placeholder.size(), replacement);
68  return s;
69  };
70 
71  fs::path home_path(home);
72 
73  if (shell == "bash") {
74  fs::path path = home_path / ".local/share/bash-completion/completions/mach3";
75  if (write_file(path, embed_path(MaCh3Program::BASH_COMPLETION)))
76  std::cout << "Installed bash completions → " << path << "\n";
77  return;
78  }
79 
80  if (shell == "zsh") {
81  fs::path path = home_path / ".local/share/zsh/site-functions/_mach3";
82  if (write_file(path, embed_path(MaCh3Program::ZSH_COMPLETION)))
83  std::cout << "Installed zsh completions → " << path << "\n";
84  return;
85  }
86  }
87 
88  void MaCh3Program::completions(const std::string& prefix, const std::vector<std::string>& context) const {
89  // Find the active subcommand from previously typed context words
90  std::string active_subcommand;
91  for (const auto& word : context) {
92  for (const auto& sub : m_subcommands) {
93  if (word == sub) {
94  active_subcommand = sub;
95  break;
96  }
97  }
98  if (!active_subcommand.empty()) break;
99  }
100 
101  if (active_subcommand.empty()) {
102  // No subcommand typed yet — complete top-level subcommand names
103  for (const auto& cmd : m_subcommands) {
104  if (cmd.rfind(prefix, 0) == 0)
105  std::cout << cmd << "\n";
106  }
107  return;
108  }
109 
110  // Find the parser for the active subcommand
111  const MaCh3ArgumentParser* sub_parser = nullptr;
112  for (const auto& [parser, mod] : m_module_map) {
113  if (parser->name() == active_subcommand) { sub_parser = parser; break; }
114  }
115  if (!sub_parser) {
116  for (const auto& [parser, plug] : m_dynamic_plugin_map) {
117  if (parser->name() == active_subcommand) { sub_parser = parser; break; }
118  }
119  }
120  if (!sub_parser) return; // unknown subcommand — fall back to files
121 
122  // If the last context word is an option that expects a value argument,
123  // output nothing so the shell falls back to file completion.
124  if (!context.empty()) {
125  const std::string& prev = context.back();
126  if (prev.size() >= 2 && prev[0] == '-') {
127  if (sub_parser->option_takes_value(prev)) {
128  return; // option takes a value — let the shell complete files
129  }
130  }
131  }
132 
133  // Complete long option names when the prefix starts with '-'
134  if (!prefix.empty() && prefix[0] == '-') {
135  for (const auto& name : sub_parser->get_long_option_names()) {
136  if (name.rfind(prefix, 0) == 0)
137  std::cout << name << "\n";
138  }
139  return;
140  }
141  // Non-option prefix (or empty) — output nothing, shell falls back to files
142  }
143 
144  // void MaCh3Program::parse_args(int argc, const char *const argv[]) {
145  // try{
146  // MaCh3ArgumentParser::parse_args(argc, argv);
147 
148  // }
149  // catch (const std::exception& err) {
150  // if (auto prefix = this->present<std::string>("--complete")) {
151  // this->completions(*prefix);
152  // exit(0);
153  // }
154 
155  // if (this->get<bool>("--install-completions")) {
156  // this->install_completions();
157  // exit(0);
158  // }
159  // std::cerr << err.what() << std::endl;
160  // std::cerr << this->get_subcommand_used();
161  // MACH3LOG_ERROR(err.what());
162  // throw;
163  // }
164 
165  // if (!this->get_subcommand_used()){
166  // std::cerr << this->get_subcommand_used();
167  // throw NoArgsException();
168  // }
169  // }
170 
172  MaCh3ArgumentParser* parser = module.get_parser();
173  this->add_subparser(*parser);
174  m_module_map[parser] = &module;
175  m_subcommands.push_back(parser->name());
176  }
177 
184  const char* env = std::getenv("MACH3_PLUGINS");
185  if (!env) {
186  //std::cerr << "No plugins specified\n";
187  return;
188  }
189 
190  std::stringstream ss(env);
191  std::string path;
192 
193  while (std::getline(ss, path, ':')) {
194  for (const auto& sofile : this->expand_plugin_path(path)) {
195  try{
196  std::unique_ptr<DynamicPlugin> dlplugin = std::make_unique<DynamicPlugin>(sofile);
197  MaCh3ArgumentParser* parser = dlplugin->get_parser();
198  if (!parser) {
199  std::cerr << "Plugin " << sofile << " did not provide a valid parser.\n";
200  continue;
201  }
202 
203  this->add_subparser(*parser);
204  m_dynamic_plugin_map[parser] = std::move(dlplugin);
205  m_subcommands.push_back(parser->name());
206  }
207  catch (const std::exception& e) {
208  std::cerr << "Failed to load plugin from " << sofile << ": " << e.what() << "\n";
209  continue;
210  }
211  catch (...) {
212  std::cerr << "Failed to load plugin from " << sofile << ": unknown error\n";
213  continue;
214  }
215  }
216  }
217  }
218 
223  m_dynamic_plugin_map.clear();
224  }
225 
232  const MaCh3ArgumentParser& sub_parser = this->get_subcommand_used();
233 
234  if (sub_parser){
235  auto plugin_itr = m_module_map.find(&sub_parser);
236  if (plugin_itr != m_module_map.end()){
237  return plugin_itr->second->Run();
238  }
239  auto dplugin_itr = m_dynamic_plugin_map.find(&sub_parser);
240  if (dplugin_itr != m_dynamic_plugin_map.end()){
241  return dplugin_itr->second->Run();
242  }
243  }
244  return 0;
245  }
246 
247 
255  std::vector<std::string> MaCh3Program::expand_plugin_path(const std::string& path) const {
256  std::vector<std::string> result;
257 
258  fs::path p(path);
259 
260  if (!fs::exists(p)) {
261  std::cerr << "Plugin path does not exist: " << path << "\n";
262  return result;
263  }
264 
265  if (fs::is_regular_file(p)) {
266  // Single .so file
267  result.push_back(path);
268  }
269  else if (fs::is_directory(p)) {
270  // Load all .so files in directory
271  for (auto& entry : fs::directory_iterator(p)) {
272  if (entry.is_regular_file() && entry.path().extension() == ".so") {
273  result.push_back(entry.path().string());
274  }
275  }
276  }
277  else {
278  std::cerr << "Invalid plugin path (not file or directory): " << path << "\n";
279  }
280 
281  return result;
282  }
283 }
MaCh3 Logging utilities built on top of SPDLOG.
Core program class for the MaCh3 command-line interface.
Interface for MaCh3 plugins and modules.
Definition: plugin.hpp:16
virtual MaCh3ArgumentParser * get_parser()=0
Get the argument parser for this plugin.
Extended ArgumentParser with MaCh3-specific functionality.
Definition: m3argparse.hpp:17
const std::string name() const
Get the name of this parser/subcommand.
Definition: m3argparse.hpp:24
std::vector< std::string > get_long_option_names() const
Return all long option names (–xxx) registered with this parser.
Definition: m3argparse.hpp:50
bool option_takes_value(const std::string &name) const
Return true if the named option expects a value (not a flag)
Definition: m3argparse.hpp:60
const MaCh3ArgumentParser & get_subcommand_used() const
Get the subcommand that was used in parsing.
Definition: m3argparse.hpp:40
void add_core_module(IModule &module)
Add a core module to the program.
void unload_dynamic_plugins()
Unload all dynamically loaded plugins.
bool write_file(const fs::path &path, std::string_view content) const
Write content to a file, creating parent directories if needed.
static constexpr std::string_view ZSH_COMPLETION
std::vector< std::string > m_subcommands
List of registered subcommand names.
void completions(const std::string &prefix, const std::vector< std::string > &context) const
Generate completion suggestions given the current prefix and preceding words.
void install_completions() const
Install shell completion scripts for bash and zsh.
static constexpr std::string_view BASH_COMPLETION
int Run()
Run the selected subcommand.
std::map< const MaCh3ArgumentParser *, IModule * > m_module_map
Map of parsers to core modules (non-owning)
std::map< const MaCh3ArgumentParser *, std::unique_ptr< DynamicPlugin > > m_dynamic_plugin_map
Map of parsers to dynamic plugins (owning)
std::string detect_shell() const
Detect the user's shell from the SHELL environment variable.
void load_dynamic_plugins()
Load dynamic plugins from the MACH3_PLUGINS environment variable.
std::vector< std::string > expand_plugin_path(const std::string &path) const
Expand a plugin path (file or directory) into a list of .so files.
Main namespace for MaCh3 software.