Line data Source code
1 : // Copyright (c) 2006-2022 Made to Order Software Corp. All Rights Reserved
2 : //
3 : // https://snapwebsites.org/project/advgetopt
4 : // contact@m2osw.com
5 : //
6 : // This program 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 2 of the License, or
9 : // (at your option) any later version.
10 : //
11 : // This program 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 along
17 : // with this program; if not, write to the Free Software Foundation, Inc.,
18 : // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 :
20 :
21 : /** \file
22 : * \brief Implementation of utility functions.
23 : *
24 : * This file includes various utility functions that are not specifically
25 : * attached to a class.
26 : */
27 :
28 : // self
29 : //
30 : #include "advgetopt/utils.h"
31 :
32 : #include "advgetopt/exception.h"
33 :
34 :
35 : // snapdev
36 : //
37 : #include <snapdev/glob_to_list.h>
38 : #include <snapdev/not_used.h>
39 : #include <snapdev/trim_string.h>
40 :
41 :
42 : // cppthread
43 : //
44 : #include <cppthread/guard.h>
45 : #include <cppthread/mutex.h>
46 :
47 :
48 : // C++ lib
49 : //
50 : #include <set>
51 :
52 :
53 : // C
54 : //
55 : #include <string.h>
56 :
57 :
58 : // last include
59 : //
60 : #include <snapdev/poison.h>
61 :
62 :
63 :
64 : namespace advgetopt
65 : {
66 :
67 :
68 :
69 : namespace
70 : {
71 :
72 :
73 :
74 : /** \brief The configuration file mutex.
75 : *
76 : * This options are generally viewed as read-only global variables. They
77 : * get setup once early on and then used and reused as many times as
78 : * required.
79 : *
80 : * This mutex makes sure that access between multiple thread happens in
81 : * a safe manner.
82 : */
83 : cppthread::mutex * g_mutex;
84 :
85 :
86 :
87 : }
88 : // no name namespace
89 :
90 :
91 :
92 : /** \brief Get a global mutex.
93 : *
94 : * This function returns a global mutex we can use to lock the advgetopt
95 : * whenever multithread functionality is required (i.e. a global is used.)
96 : *
97 : * It is safe to call this function early (i.e. before main was ever
98 : * called.)
99 : *
100 : * Usage:
101 : *
102 : * \code
103 : * cppthread::guard lock(get_global_mutex());
104 : * \endcode
105 : *
106 : * \return A reference to our global mutex.
107 : */
108 7977 : cppthread::mutex & get_global_mutex()
109 : {
110 : {
111 15954 : cppthread::guard lock(*cppthread::g_system_mutex);
112 :
113 7977 : if(g_mutex == nullptr)
114 : {
115 1 : g_mutex = new cppthread::mutex();
116 : }
117 : }
118 :
119 7977 : return *g_mutex;
120 : }
121 :
122 :
123 :
124 : /** \brief Remove single (') or double (") quotes from a string.
125 : *
126 : * If a string starts and ends with the same quotation mark, then it
127 : * gets removed.
128 : *
129 : * If no quotes appear, then the function returns a copy of the input as is.
130 : *
131 : * The \p pairs parameter must have an even size (or the last character
132 : * gets ignored). By default, it is set to the double and single quotes:
133 : *
134 : * \code
135 : * "\"\"''"
136 : * \endcode
137 : *
138 : * To remove square, angle, curly brackets:
139 : *
140 : * \code
141 : * "[]<>{}"
142 : * \endcode
143 : *
144 : * \todo
145 : * Add support for UTF-8 quotes. Right now only quotes of 1 byte will
146 : * work.
147 : *
148 : * \param[in] s The string to unquote.
149 : * \param[in] pairs A list of accepted quotes.
150 : *
151 : * \return The unquoted string.
152 : */
153 898 : std::string unquote(std::string const & s, std::string const & pairs)
154 : {
155 898 : if(s.length() >= 2)
156 : {
157 828 : std::string::size_type const max(pairs.length() - 1);
158 2356 : for(std::string::size_type pos(0); pos < max; pos += 2)
159 : {
160 3216 : if(s.front() == pairs[pos + 0]
161 1608 : && s.back() == pairs[pos + 1])
162 : {
163 80 : return s.substr(1, s.length() - 2);
164 : }
165 : }
166 : }
167 :
168 818 : return s;
169 : }
170 :
171 :
172 : /** \brief Split a string in sub-strings separated by \p separators.
173 : *
174 : * This function searches for any of the \p separators in \p str and
175 : * split at those locations.
176 : *
177 : * For example, to split a comma separated list of strings, use the
178 : * following:
179 : *
180 : * \code
181 : * string_list_t result;
182 : * option_info::split_string(string_to_split, result, {","});
183 : * \endcode
184 : *
185 : * If `string_to_split` is set to "a, b, c", then the `result` vector
186 : * will have three strings as a result: `a`, `b`, and `c`. Note that
187 : * the function automatically trims all strings and it never keeps
188 : * empty strings. So two separators one after another is accepted and
189 : * no empty string results.
190 : *
191 : * The trimming happens after the split occurs. This allows for the
192 : * list of separators to include spaces as separators.
193 : *
194 : * The function does not clear the result vector. This allows you to
195 : * call this function multiple times with various strings and the
196 : * results will be cumulated.
197 : *
198 : * \note
199 : * This function is a static so it can be used from anywhere to split
200 : * strings as required. You do not need to have an option_info instance.
201 : *
202 : * \todo
203 : * See to fix the fact that `a"b"c` becomes `{"a", "b", "c"}` when
204 : * there are not separators between `a`, `"b"`, and `c`. To the minimum
205 : * we may want to generate an error when such is found (i.e. when a
206 : * quote is found and `start < pos` is true.
207 : *
208 : * \param[in] str The string to split.
209 : * \param[in] result The vector where the split strings are saved.
210 : * \param[in] separators The vector of strings used as separators.
211 : */
212 147 : void split_string(std::string const & str
213 : , string_list_t & result
214 : , string_list_t const & separators)
215 : {
216 147 : std::string::size_type pos(0);
217 147 : std::string::size_type start(0);
218 17021 : while(pos < str.length())
219 : {
220 8437 : if(str[pos] == '\'' || str[pos] == '"')
221 : {
222 14 : if(start < pos)
223 : {
224 12 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
225 6 : if(!v.empty())
226 : {
227 4 : result.push_back(v);
228 : }
229 6 : start = pos;
230 : }
231 :
232 : // quoted parameters are handled without the separators
233 : //
234 14 : char const quote(str[pos]);
235 14 : for(++pos; pos < str.length() && str[pos] != quote; ++pos);
236 :
237 28 : std::string const v(str.substr(start + 1, pos - (start + 1)));
238 14 : if(!v.empty())
239 : {
240 11 : result.push_back(v);
241 : }
242 14 : if(pos < str.length())
243 : {
244 : // skip the closing quote
245 : //
246 12 : ++pos;
247 : }
248 14 : start = pos;
249 : }
250 : else
251 : {
252 8423 : bool found(false);
253 16415 : for(auto const & sep : separators)
254 : {
255 8440 : if(str.length() - pos >= sep.length())
256 : {
257 8440 : if(str.compare(pos, sep.length(), sep) == 0)
258 : {
259 : // match! cut here
260 : //
261 448 : if(start < pos)
262 : {
263 852 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
264 426 : if(!v.empty())
265 : {
266 426 : result.push_back(v);
267 : }
268 : }
269 448 : pos += sep.length();
270 448 : start = pos;
271 448 : found = true;
272 448 : break;
273 : }
274 : }
275 : }
276 :
277 8423 : if(!found)
278 : {
279 7975 : ++pos;
280 : }
281 : }
282 : }
283 :
284 147 : if(start < pos)
285 : {
286 280 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
287 140 : if(!v.empty())
288 : {
289 140 : result.push_back(v);
290 : }
291 : }
292 147 : }
293 :
294 :
295 : /** \brief Insert the group (or project) name in the filename.
296 : *
297 : * This function inserts the name of the group in the specified full path
298 : * filename. It gets added right before the basename. So for example you
299 : * have a path such as:
300 : *
301 : * /etc/snapwebsites/advgetopt.conf
302 : *
303 : * and a group name such as:
304 : *
305 : * adventure
306 : *
307 : * The resulting path is:
308 : *
309 : * /etc/snapwebsites/adventure.d/advgetopt.conf
310 : *
311 : * Notice that the function adds a ".d" as well.
312 : *
313 : * If the group name is empty or null, then the project name is used. If
314 : * both are empty, then nothing happens (the function returns an empty list).
315 : *
316 : * \exception getopt_root_filename
317 : * The \p filename parameter cannot be a file in the root directory.
318 : *
319 : * \param[in] filename The filename where the project name gets injected.
320 : * \param[in] group_name The name of the group to inject in the filename.
321 : * \param[in] project_name The name of the project to inject in the filename.
322 : *
323 : * \return The list of filenames or an empty list if no group or project name
324 : * or filename were specified.
325 : */
326 535 : string_list_t insert_group_name(
327 : std::string const & filename
328 : , char const * group_name
329 : , char const * project_name)
330 : {
331 535 : if(filename.empty())
332 : {
333 5 : return string_list_t();
334 : }
335 :
336 1060 : std::string name;
337 530 : if(group_name == nullptr
338 162 : || *group_name == '\0')
339 : {
340 372 : if(project_name == nullptr
341 370 : || *project_name == '\0')
342 : {
343 4 : return string_list_t();
344 : }
345 368 : name = project_name;
346 : }
347 : else
348 : {
349 158 : name = group_name;
350 : }
351 :
352 1052 : std::string pattern;
353 526 : std::string::size_type const pos(filename.find_last_of('/'));
354 526 : if(pos == 0)
355 : {
356 0 : throw getopt_root_filename("filename \"" + filename + "\" starts with a slash (/), which is not allowed.");
357 : }
358 526 : if(pos != std::string::npos
359 250 : && pos > 0)
360 : {
361 1250 : pattern = filename.substr(0, pos + 1)
362 750 : + name
363 750 : + ".d/[0-9][0-9]-"
364 1000 : + filename.substr(pos + 1);
365 : }
366 : else
367 : {
368 552 : pattern = name
369 552 : + (".d/[0-9][0-9]-" + filename);
370 : }
371 :
372 : // we use an std::set so the resulting list is sorted
373 : //
374 1052 : snapdev::glob_to_list<std::set<std::string>> glob;
375 :
376 : // the glob() function is not thread safe
377 : {
378 1052 : cppthread::guard lock(get_global_mutex());
379 526 : snapdev::NOT_USED(glob.read_path<snapdev::glob_to_list_flag_t::GLOB_FLAG_IGNORE_ERRORS>(pattern));
380 : }
381 :
382 : // we add the default name if none other exists
383 : //
384 526 : if(glob.empty())
385 : {
386 504 : if(pos != std::string::npos
387 228 : && pos > 0)
388 : {
389 1140 : glob.insert(filename.substr(0, pos + 1)
390 684 : + name
391 684 : + ".d/50-"
392 912 : + filename.substr(pos + 1));
393 : }
394 : else
395 : {
396 552 : glob.insert(name
397 552 : + (".d/50-" + filename));
398 : }
399 : }
400 :
401 526 : return string_list_t(glob.begin(), glob.end());
402 : }
403 :
404 :
405 : /** \brief Generate the default filename (the ".../50-...")
406 : *
407 : * This function generates the default filename as the insert_group_name()
408 : * expects to find in the configuration sub-directory.
409 : *
410 : * The name is formed as follow:
411 : *
412 : * <path> / <directory> ".d" / <priority> "-" <basename>
413 : *
414 : * Where `<path>` is the path found in \p filename. If no path is defined in
415 : * \p filename, then the `<path> /` part is not prepended:
416 : *
417 : * <directory> ".d" / <priority> "-" <basename>
418 : *
419 : * Where `<directory>` is the \p group_name if defined, otherwise it uses
420 : * the \p project_name. This is why if neither is defined, then the function
421 : * immediately returns an empty string.
422 : *
423 : * Where `<priority>` is a number from 0 to 99 inclusive. This is used to
424 : * sort the files before processing them. File with lower priorities are
425 : * loaded first. Parameters found in files with higher priorities overwrite
426 : * the values of parameters found in files with lower priorities.
427 : *
428 : * Where `<basename>` is the end of \p filename, the part after the last
429 : * slash (`/`). If \p filename is not empty and it does not include a slash
430 : * then the entire \p filename is taken as the `<basename>`. Note that
431 : * \p filename is expected to include an extension such as `.conf`. The
432 : * extension is not modified in any way.
433 : *
434 : * Since the result is not viable when \p filename is empty, the function
435 : * immediately returns an empty string in that situation.
436 : *
437 : * \exception getopt_root_filename
438 : * The \p filename parameter cannot be a file in the root directory.
439 : *
440 : * \param[in] filename The filename where the project name gets injected.
441 : * \param[in] group_name The name of the group to inject in the filename.
442 : * \param[in] project_name The name of the project to inject in the filename.
443 : * \param[in] priority The priority of the new file (0 to 99).
444 : *
445 : * \return The default filenames or an empty list if no group or project
446 : * or file name were specified.
447 : */
448 0 : std::string default_group_name(
449 : std::string const & filename
450 : , char const * group_name
451 : , char const * project_name
452 : , int priority)
453 : {
454 0 : if(priority < 0 || priority >= 100)
455 : {
456 : throw getopt_invalid_parameter(
457 : "priority must be a number between 0 and 99 inclusive; "
458 0 : + std::to_string(priority)
459 0 : + " is invalid.");
460 : }
461 :
462 0 : if(filename.empty())
463 : {
464 0 : return std::string();
465 : }
466 :
467 0 : char const * name(nullptr);
468 0 : if(group_name == nullptr
469 0 : || *group_name == '\0')
470 : {
471 0 : if(project_name == nullptr
472 0 : || *project_name == '\0')
473 : {
474 0 : return std::string();
475 : }
476 0 : name = project_name;
477 : }
478 : else
479 : {
480 0 : name = group_name;
481 : }
482 :
483 0 : std::string::size_type const pos(filename.find_last_of('/'));
484 0 : if(pos == 0)
485 : {
486 0 : throw getopt_root_filename("filename \"" + filename + "\" starts with a slash (/), which is not allowed.");
487 : }
488 :
489 0 : std::string result;
490 0 : result.reserve(filename.length() + strlen(name) + 6);
491 0 : if(pos != std::string::npos)
492 : {
493 0 : result = filename.substr(0, pos + 1);
494 : }
495 0 : result += name;
496 0 : result += ".d/";
497 0 : if(priority < 10)
498 : {
499 0 : result += '0';
500 : }
501 0 : result += std::to_string(priority);
502 0 : result += '-';
503 0 : if(pos == std::string::npos)
504 : {
505 0 : result += filename;
506 : }
507 : else
508 : {
509 0 : result += filename.substr(pos + 1);
510 : }
511 :
512 0 : return result;
513 : }
514 :
515 :
516 : /** \brief Replace a starting `~/...` with the contents of the \$HOME variable.
517 : *
518 : * This function checks the beginning of \p filename. If it starts with `'~/'`
519 : * then it replaces the `'~'` character with the contents of the \$HOME
520 : * environment variable.
521 : *
522 : * If \p filename is just `"~"`, then the function returns the contents of
523 : * the \$HOME environment variable by itself.
524 : *
525 : * If somehow the \$HOME environment variable is empty, the function does
526 : * nothing.
527 : *
528 : * \todo
529 : * Add support for "~<user name>/..." so that way a service could use its
530 : * own home folder even when run from a different user (a.k.a. root). This
531 : * requires that we load the user database and get the home folder from that
532 : * data.
533 : *
534 : * \param[in] filename The filename to check for a tilde (~).
535 : *
536 : * \return The input as is unless the \$HOME path can be prepended to replace
537 : * the tilde (~) character.
538 : */
539 574 : std::string handle_user_directory(std::string const & filename)
540 : {
541 1148 : if(!filename.empty()
542 574 : && filename[0] == '~'
543 637 : && (filename.length() == 1 || filename[1] == '/'))
544 : {
545 63 : char const * const home(getenv("HOME"));
546 63 : if(home != nullptr
547 59 : && *home != '\0')
548 : {
549 57 : return home + filename.substr(1);
550 : }
551 : }
552 :
553 517 : return filename;
554 : }
555 :
556 :
557 : /** \brief Check whether a value represents "true".
558 : *
559 : * This function checks a string to see whether it is one of:
560 : *
561 : * * "true"
562 : * * "on"
563 : * * "yes"
564 : * * "1"
565 : *
566 : * If so, then the function returns true.
567 : *
568 : * \param[in] s The string to be checked.
569 : *
570 : * \return true if the string represents "true".
571 : */
572 9 : bool is_true(std::string s)
573 : {
574 9 : return s == "true" || s == "on" || s == "yes" | s == "1";
575 : }
576 :
577 :
578 : /** \brief Check whether a value represents "false".
579 : *
580 : * This function checks a string to see whether it is one of:
581 : *
582 : * * "false"
583 : * * "off"
584 : * * "no"
585 : * * "0"
586 : *
587 : * If so, then the function returns true.
588 : *
589 : * \param[in] s The string to be checked.
590 : *
591 : * \return true if the string represents "false".
592 : */
593 11 : bool is_false(std::string s)
594 : {
595 11 : return s == "false" || s == "off" || s == "no" || s == "0";
596 : }
597 :
598 :
599 :
600 6 : } // namespace advgetopt
601 : // vim: ts=4 sw=4 et
|