1 /*  Copyright (C) 2012-2021 by László Nagy
2     This file is part of Bear.
3 
4     Bear is a tool to generate compilation database for clang tooling.
5 
6     Bear is free software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     Bear is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "libflags/Flags.h"
21 
22 #include <cstring>
23 #include <iostream>
24 #include <optional>
25 #include <set>
26 #include <tuple>
27 #include <utility>
28 
29 #include <fmt/format.h>
30 
31 namespace {
32 
33     constexpr char QUERY_GROUP[] = "query options";
34 
35     std::optional<std::tuple<const char**, const char**>>
take(const flags::Option & option,const char ** const begin,const char ** const end)36     take(const flags::Option& option, const char** const begin, const char** const end) noexcept
37     {
38         return (option.arguments < 0)
39             ? std::optional(std::make_tuple(begin, end))
40             : (begin + option.arguments > end)
41                 ? std::nullopt
42                 : std::optional(std::make_tuple(begin, begin + option.arguments));
43     }
44 
order_by_relevance(const flags::OptionMap & options,const std::optional<std::string_view> & group)45     std::list<flags::OptionValue> order_by_relevance(const flags::OptionMap& options, const std::optional<std::string_view>& group)
46     {
47         std::list<flags::OptionValue> result;
48         std::copy_if(std::begin(options), std::end(options),
49             std::back_inserter(result),
50             [&group](auto& option) { return option.second.group_name == group && option.second.arguments >= 0; });
51         std::copy_if(std::begin(options), std::end(options),
52             std::back_inserter(result),
53             [&group](auto& option) { return option.second.group_name == group && option.second.arguments < 0; });
54         return result;
55     }
56 
group_by(const flags::OptionMap & options)57     std::list<std::list<flags::OptionValue>> group_by(const flags::OptionMap& options)
58     {
59         // find out what are the option groups.
60         std::set<std::optional<std::string_view>> groups;
61         for (const auto& [_, option] : options) {
62             groups.emplace(option.group_name);
63         }
64         std::list<std::list<flags::OptionValue>> result;
65         // insert to the result list.
66         for (auto& group : groups) {
67             result.emplace_back(order_by_relevance(options, group));
68         }
69         return result;
70     }
71 
format_parameters(const flags::Option & option)72     std::string format_parameters(const flags::Option& option)
73     {
74         switch (option.arguments) {
75         case 0:
76             return "";
77         case 1:
78             return " <arg>";
79         case 2:
80             return " <arg0> <arg1>>";
81         case 3:
82             return " <arg0> <arg1> <arg2>";
83         default:
84             return " ...";
85         }
86     }
87 
format_options(std::ostream & os,const std::list<flags::OptionValue> & options)88     void format_options(std::ostream& os, const std::list<flags::OptionValue>& options)
89     {
90         for (auto& it : options) {
91             const auto& [flag, option] = it;
92 
93             const std::string parameters = format_parameters(option);
94             const std::string short_help = option.required
95                 ? fmt::format(" {0}{1}", flag, parameters)
96                 : fmt::format(" [{0}{1}]", flag, parameters);
97             os << short_help;
98         }
99     }
100 
format_options_long(std::ostream & os,const std::list<flags::OptionValue> & main_options)101     void format_options_long(std::ostream& os, const std::list<flags::OptionValue>& main_options)
102     {
103         for (auto& it : main_options) {
104             const auto& [flag, option] = it;
105 
106             const std::string flag_name = fmt::format("  {0}{1}", flag, format_parameters(option));
107             const size_t flag_size = flag_name.length();
108 
109             // print flag name
110             os << flag_name;
111             // decide if the help text goes into the same line or not
112             if (flag_size > 22) {
113                 os << std::endl
114                    << std::string(15, ' ');
115             } else {
116                 os << std::string(23 - flag_size, ' ');
117             }
118             os << option.help;
119             // print default value if exists
120             if (option.default_value) {
121                 os << " (default: " << option.default_value.value() << ')';
122             }
123             os << std::endl;
124         }
125     }
126 }
127 
128 namespace flags {
129 
Arguments(std::string_view program,Arguments::Parameters && parameters)130     Arguments::Arguments(std::string_view program, Arguments::Parameters&& parameters)
131             : program_(program)
132             , parameters_(parameters)
133     { }
134 
as_bool(const std::string_view & key) const135     rust::Result<bool> Arguments::as_bool(const std::string_view& key) const
136     {
137         return rust::Ok(parameters_.find(key) != parameters_.end());
138     }
139 
as_string(const std::string_view & key) const140     rust::Result<std::string_view> Arguments::as_string(const std::string_view& key) const
141     {
142         if (auto values = parameters_.find(key); values != parameters_.end()) {
143             return (values->second.size() == 1)
144                 ? (rust::Ok(values->second.front()))
145                 : rust::Result<std::string_view>(
146                     rust::Err(std::runtime_error(
147                         fmt::format("Parameter \"{0}\" is not a single string.", key))));
148         }
149         return rust::Result<std::string_view>(
150             rust::Err(std::runtime_error(
151                 fmt::format("Parameter \"{0}\" is not available.", key))));
152     }
153 
as_string_list(const std::string_view & key) const154     rust::Result<std::vector<std::string_view>> Arguments::as_string_list(const std::string_view& key) const
155     {
156         if (auto values = parameters_.find(key); values != parameters_.end()) {
157             return rust::Ok(values->second);
158         }
159         return rust::Result<std::vector<std::string_view>>(
160             rust::Err(std::runtime_error(
161                 fmt::format("Parameter \"{0}\" is not available.", key))));
162     }
163 
operator <<(std::ostream & os,const Arguments & args)164     std::ostream& operator<<(std::ostream& os, const Arguments& args)
165     {
166         os << '{';
167         os << "program: " << args.program_ << ", arguments: [";
168         for (auto arg_it = args.parameters_.begin(); arg_it != args.parameters_.end(); ++arg_it) {
169             if (arg_it != args.parameters_.begin()) {
170                 os << ", ";
171             }
172             os << '{' << arg_it->first << ": [";
173             for (auto param_it = arg_it->second.begin(); param_it != arg_it->second.end(); ++param_it) {
174                 if (param_it != arg_it->second.begin()) {
175                     os << ", ";
176                 }
177                 os << *param_it;
178             }
179             os << "]}";
180         }
181         os << "]}";
182         return os;
183     }
184 
Parser(std::string_view name,std::string_view version,std::initializer_list<OptionValue> options)185     Parser::Parser(std::string_view name, std::string_view version, std::initializer_list<OptionValue> options)
186             : name_(name)
187             , version_(version)
188             , options_(options)
189             , commands_()
190     {
191         options_.insert({ VERBOSE, { 0, false, "run in verbose mode", std::nullopt, std::nullopt } });
192         options_.insert({ HELP, { 0, false, "print help and exit", std::nullopt, { QUERY_GROUP } } });
193         options_.insert({ VERSION, { 0, false, "print version and exit", std::nullopt, { QUERY_GROUP } } });
194     }
195 
Parser(std::string_view name,std::initializer_list<OptionValue> options)196     Parser::Parser(std::string_view name, std::initializer_list<OptionValue> options)
197             : name_(name)
198             , version_()
199             , options_(options)
200             , commands_()
201     {
202         options_.insert({ VERBOSE, { 0, false, "run in verbose mode", std::nullopt, std::nullopt } });
203         options_.insert({ HELP, { 0, false, "print help and exit", std::nullopt, { QUERY_GROUP } } });
204     }
205 
Parser(std::string_view name,std::string_view version,std::initializer_list<Parser> commands)206     Parser::Parser(std::string_view name, std::string_view version, std::initializer_list<Parser> commands)
207             : name_(name)
208             , version_(version)
209             , options_()
210             , commands_(commands)
211     {
212         options_.insert({ HELP, { 0, false, "print help and exit", std::nullopt, { QUERY_GROUP } } });
213         options_.insert({ VERSION, { 0, false, "print version and exit", std::nullopt, { QUERY_GROUP } } });
214     }
215 
parse(const int argc,const char ** argv) const216     rust::Result<Arguments> Parser::parse(const int argc, const char** argv) const
217     {
218         if (argc < 1 || argv == nullptr) {
219             return rust::Err(std::runtime_error("Empty argument list."));
220         }
221 
222         if (!commands_.empty() && argc >= 2) {
223             const std::string_view command = argv[1];
224             const auto sub_command = std::find_if(commands_.begin(), commands_.end(),
225                                            [&command](auto candidate) { return candidate.name_ == command; });
226             if (sub_command != commands_.end()) {
227                 return sub_command->parse(argc - 1, argv + 1)
228                         .map<Arguments>([&sub_command](auto arguments) {
229                             arguments.parameters_[COMMAND] = {sub_command->name_};
230                             return arguments;
231                         });
232             }
233         }
234 
235         std::string_view program(argv[0]);
236         Arguments::Parameters parameters;
237 
238         const char** const args_end = argv + argc;
239         for (const char** args_it = ++argv; args_it != args_end;) {
240             // find which option is it.
241             if (auto option = options_.find(*args_it); option != options_.end()) {
242                 // take the required number of arguments if founded.
243                 if (const auto params = take(option->second, args_it + 1, args_end); params) {
244                     const auto& [begin, end] = params.value();
245                     auto args = std::vector<std::string_view>(begin, end);
246 
247                     if (auto it = parameters.find(option->first); parameters.end() != it) {
248                         std::copy(args.begin(), args.end(), std::back_inserter(it->second));
249                     } else {
250                         parameters.emplace(option->first, args);
251                     }
252 
253                     args_it = end;
254                 } else {
255                     return rust::Err(std::runtime_error(
256                         fmt::format("Not enough parameters for: \"{0}\"", *args_it)));
257                 }
258             } else {
259                 return rust::Err(std::runtime_error(
260                     fmt::format("Unrecognized parameter: \"{0}\"", *args_it)));
261             }
262         }
263         // add default values to the parameters as it would given by the user.
264         for (const auto& [flag, option] : options_) {
265             if (option.default_value.has_value() && parameters.find(flag) == parameters.end()) {
266                 std::vector<std::string_view> args = { option.default_value.value() };
267                 parameters.emplace(flag, args);
268             }
269         }
270         // if this is not a help or version query, then validate the parameters strict.
271         if (parameters.find(HELP) == parameters.end() && parameters.find(VERSION) == parameters.end()) {
272             for (const auto& [flag, option] : options_) {
273                 // check if the parameter is required, but not present.
274                 if (option.required && parameters.find(flag) == parameters.end()) {
275                     return rust::Err(std::runtime_error(
276                         fmt::format("Parameter is required, but not given: \"{0}\"", flag)));
277                 }
278             }
279         }
280         return rust::Ok(Arguments(program, std::move(parameters)));
281     }
282 
parse_or_exit(int argc,const char ** argv) const283     rust::Result<Arguments> Parser::parse_or_exit(int argc, const char** argv) const
284     {
285         auto sub_command = [this](const std::string_view &name) -> const Parser * {
286             const auto it = std::find_if(commands_.begin(), commands_.end(),
287                                          [&name](auto command) { return command.name_ == name; });
288             return (it != commands_.end()) ? &(*it) : nullptr;
289         };
290 
291         return parse(argc, argv)
292             // print error if anything bad happens.
293             .on_error([this](auto error) {
294                 std::cerr << error.what() << std::endl;
295                 print_usage(nullptr, std::cerr);
296                 exit(EXIT_FAILURE);
297             })
298             // if parsing success, check for the `--help` and `--version` flags
299             .on_success([this, &sub_command](auto args) {
300                 // print version message and exit zero
301                 if (args.as_bool(VERSION).unwrap_or(false)) {
302                     print_version(std::cout);
303                     exit(EXIT_SUCCESS);
304                 }
305                 // print help message and exit zero
306                 if (args.as_bool(HELP).unwrap_or(false)) {
307                     if (const auto command = args.as_string(COMMAND); command.is_ok()) {
308                         print_help(sub_command(command.unwrap()), std::cout);
309                     } else {
310                         print_help(nullptr, std::cout);
311                     }
312                     exit(EXIT_SUCCESS);
313                 }
314             });
315     }
316 
print_help(const Parser * sub_command,std::ostream & os) const317     void Parser::print_help(const Parser *sub_command, std::ostream& os) const {
318         print_usage(sub_command, os);
319 
320         const Parser &parser = (sub_command != nullptr) ? *sub_command : *this;
321         // print commands if exists.
322         if (!parser.commands_.empty()) {
323             os << std::endl << "commands" << std::endl;
324             for (const auto& command : parser.commands_) {
325                 os << "  " << command.name_ << std::endl;
326             }
327         }
328         // print options
329         const std::list<std::list<flags::OptionValue>> options = group_by(parser.options_);
330         for (const auto& group : options) {
331             os << std::endl;
332             if (auto group_name = group.front().second.group_name; group_name) {
333                 os << group_name.value() << std::endl;
334             }
335             format_options_long(os, group);
336         }
337     }
338 
print_usage(const Parser * sub_command,std::ostream & os) const339     void Parser::print_usage(const Parser *sub_command, std::ostream& os) const {
340         os << "Usage: " << name_;
341         // check for the given command
342         if (sub_command != nullptr) {
343             os << " " << sub_command->name_;
344             const auto options = order_by_relevance(sub_command->options_, std::nullopt);
345             format_options(os, options);
346         } else {
347             if (!commands_.empty()) {
348                 os << " <command>";
349             }
350             const auto options = order_by_relevance(options_, std::nullopt);
351             format_options(os, options);
352         }
353         os << std::endl;
354     }
355 
print_version(std::ostream & os) const356     void Parser::print_version(std::ostream& os) const
357     {
358         os << name_ << " " << version_ << std::endl;
359     }
360 }
361