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++
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 8553 : cppthread::mutex & get_global_mutex()
109 : {
110 : {
111 17106 : cppthread::guard lock(*cppthread::g_system_mutex);
112 :
113 8553 : if(g_mutex == nullptr)
114 : {
115 1 : g_mutex = new cppthread::mutex();
116 : }
117 : }
118 :
119 8553 : 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 1070 : std::string unquote(std::string const & s, std::string const & pairs)
154 : {
155 1070 : if(s.length() >= 2)
156 : {
157 946 : std::string::size_type const max(pairs.length() - 1);
158 2707 : for(std::string::size_type pos(0); pos < max; pos += 2)
159 : {
160 3688 : if(s.front() == pairs[pos + 0]
161 1844 : && s.back() == pairs[pos + 1])
162 : {
163 83 : return s.substr(1, s.length() - 2);
164 : }
165 : }
166 : }
167 :
168 987 : 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 161 : void split_string(std::string const & str
213 : , string_list_t & result
214 : , string_list_t const & separators)
215 : {
216 161 : std::string::size_type pos(0);
217 161 : std::string::size_type start(0);
218 18257 : while(pos < str.length())
219 : {
220 9048 : if(str[pos] == '\'' || str[pos] == '"')
221 : {
222 9 : if(start < pos)
223 : {
224 8 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
225 4 : if(!v.empty())
226 : {
227 4 : result.push_back(v);
228 : }
229 4 : start = pos;
230 : }
231 :
232 : // quoted parameters are handled without the separators
233 : //
234 9 : char const quote(str[pos]);
235 9 : for(++pos; pos < str.length() && str[pos] != quote; ++pos);
236 :
237 18 : std::string const v(str.substr(start + 1, pos - (start + 1)));
238 9 : if(!v.empty())
239 : {
240 6 : result.push_back(v);
241 : }
242 9 : if(pos < str.length())
243 : {
244 : // skip the closing quote
245 : //
246 7 : ++pos;
247 : }
248 9 : start = pos;
249 : }
250 : else
251 : {
252 9039 : bool found(false);
253 17633 : for(auto const & sep : separators)
254 : {
255 9056 : if(str.length() - pos >= sep.length())
256 : {
257 9056 : if(str.compare(pos, sep.length(), sep) == 0)
258 : {
259 : // match! cut here
260 : //
261 462 : if(start < pos)
262 : {
263 884 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
264 442 : if(!v.empty())
265 : {
266 442 : result.push_back(v);
267 : }
268 : }
269 462 : pos += sep.length();
270 462 : start = pos;
271 462 : found = true;
272 462 : break;
273 : }
274 : }
275 : }
276 :
277 9039 : if(!found)
278 : {
279 8577 : ++pos;
280 : }
281 : }
282 : }
283 :
284 161 : if(start < pos)
285 : {
286 314 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
287 157 : if(!v.empty())
288 : {
289 157 : result.push_back(v);
290 : }
291 : }
292 161 : }
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 536 : string_list_t insert_group_name(
327 : std::string const & filename
328 : , char const * group_name
329 : , char const * project_name)
330 : {
331 536 : if(filename.empty())
332 : {
333 5 : return string_list_t();
334 : }
335 :
336 1062 : std::string name;
337 531 : if(group_name == nullptr
338 163 : || *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 159 : name = group_name;
350 : }
351 :
352 1054 : std::string pattern;
353 527 : std::string::size_type const pos(filename.find_last_of('/'));
354 527 : if(pos == 0)
355 : {
356 1 : throw getopt_root_filename("filename \"" + filename + "\" last slash (/) is at the start, 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 : glob.insert(default_group_name(
387 : filename
388 : , group_name
389 : , project_name));
390 : }
391 :
392 526 : return string_list_t(glob.begin(), glob.end());
393 : }
394 :
395 :
396 : /** \brief Generate the default filename (the ".../50-...")
397 : *
398 : * This function generates the default filename as the insert_group_name()
399 : * expects to find in the configuration sub-directory.
400 : *
401 : * The name is formed as follow:
402 : *
403 : * <path> / <directory> ".d" / <priority> "-" <basename>
404 : *
405 : * Where `<path>` is the path found in \p filename. If no path is defined in
406 : * \p filename, then the `<path> /` part is not prepended:
407 : *
408 : * <directory> ".d" / <priority> "-" <basename>
409 : *
410 : * Where `<directory>` is the \p group_name if defined, otherwise it uses
411 : * the \p project_name. This is why if neither is defined, then the function
412 : * immediately returns an empty string.
413 : *
414 : * Where `<priority>` is a number from 0 to 99 inclusive. This is used to
415 : * sort the files before processing them. File with lower priorities are
416 : * loaded first. Parameters found in files with higher priorities overwrite
417 : * the values of parameters found in files with lower priorities.
418 : *
419 : * Where `<basename>` is the end of \p filename, the part after the last
420 : * slash (`/`). If \p filename is not empty and it does not include a slash
421 : * then the entire \p filename is taken as the `<basename>`. Note that
422 : * \p filename is expected to include an extension such as `.conf`. The
423 : * extension is not modified in any way.
424 : *
425 : * Since the result is not viable when \p filename is empty, the function
426 : * immediately returns an empty string in that situation.
427 : *
428 : * \exception getopt_root_filename
429 : * The \p filename parameter cannot be a file in the root directory.
430 : *
431 : * \param[in] filename The filename where the project name gets injected.
432 : * \param[in] group_name The name of the group to inject in the filename.
433 : * \param[in] project_name The name of the project to inject in the filename.
434 : * \param[in] priority The priority of the new file (0 to 99).
435 : *
436 : * \return The default filenames or an empty list if no group or project
437 : * or file name were specified.
438 : */
439 666 : std::string default_group_name(
440 : std::string const & filename
441 : , char const * group_name
442 : , char const * project_name
443 : , int priority)
444 : {
445 666 : if(priority < 0 || priority >= 100)
446 : {
447 : throw getopt_invalid_parameter(
448 : "priority must be a number between 0 and 99 inclusive; "
449 80 : + std::to_string(priority)
450 120 : + " is invalid.");
451 : }
452 :
453 626 : if(filename.empty())
454 : {
455 5 : return std::string();
456 : }
457 :
458 621 : char const * name(nullptr);
459 621 : if(group_name == nullptr
460 269 : || *group_name == '\0')
461 : {
462 358 : if(project_name == nullptr
463 356 : || *project_name == '\0')
464 : {
465 4 : return std::string();
466 : }
467 354 : name = project_name;
468 : }
469 : else
470 : {
471 263 : name = group_name;
472 : }
473 :
474 617 : std::string::size_type const pos(filename.find_last_of('/'));
475 617 : if(pos == 0)
476 : {
477 1 : throw getopt_root_filename("filename \"" + filename + "\" starts with a slash (/), which is not allowed.");
478 : }
479 :
480 1232 : std::string result;
481 616 : result.reserve(filename.length() + strlen(name) + 6);
482 616 : if(pos != std::string::npos)
483 : {
484 338 : result = filename.substr(0, pos + 1);
485 : }
486 616 : result += name;
487 616 : result += ".d/";
488 616 : if(priority < 10)
489 : {
490 10 : result += '0';
491 : }
492 616 : result += std::to_string(priority);
493 616 : result += '-';
494 616 : if(pos == std::string::npos)
495 : {
496 278 : result += filename;
497 : }
498 : else
499 : {
500 338 : result += filename.substr(pos + 1);
501 : }
502 :
503 616 : return result;
504 : }
505 :
506 :
507 : /** \brief Replace a starting `~/...` with the contents of the \$HOME variable.
508 : *
509 : * This function checks the beginning of \p filename. If it starts with `'~/'`
510 : * then it replaces the `'~'` character with the contents of the \$HOME
511 : * environment variable.
512 : *
513 : * If \p filename is just `"~"`, then the function returns the contents of
514 : * the \$HOME environment variable by itself.
515 : *
516 : * If somehow the \$HOME environment variable is empty, the function does
517 : * nothing.
518 : *
519 : * \todo
520 : * Add support for "~<user name>/..." so that way a service could use its
521 : * own home folder even when run from a different user (a.k.a. root). This
522 : * requires that we load the user database and get the home folder from that
523 : * data.
524 : *
525 : * \param[in] filename The filename to check for a tilde (~).
526 : *
527 : * \return The input as is unless the \$HOME path can be prepended to replace
528 : * the tilde (~) character.
529 : */
530 574 : std::string handle_user_directory(std::string const & filename)
531 : {
532 1148 : if(!filename.empty()
533 574 : && filename[0] == '~'
534 637 : && (filename.length() == 1 || filename[1] == '/'))
535 : {
536 63 : char const * const home(getenv("HOME"));
537 63 : if(home != nullptr
538 59 : && *home != '\0')
539 : {
540 57 : return home + filename.substr(1);
541 : }
542 : }
543 :
544 517 : return filename;
545 : }
546 :
547 :
548 : /** \brief Check whether a value represents "true".
549 : *
550 : * This function checks a string to see whether it is one of:
551 : *
552 : * * "true"
553 : * * "on"
554 : * * "yes"
555 : * * "1"
556 : *
557 : * If so, then the function returns true.
558 : *
559 : * \param[in] s The string to be checked.
560 : *
561 : * \return true if the string represents "true".
562 : */
563 9 : bool is_true(std::string s)
564 : {
565 9 : return s == "true" || s == "on" || s == "yes" | s == "1";
566 : }
567 :
568 :
569 : /** \brief Check whether a value represents "false".
570 : *
571 : * This function checks a string to see whether it is one of:
572 : *
573 : * * "false"
574 : * * "off"
575 : * * "no"
576 : * * "0"
577 : *
578 : * If so, then the function returns true.
579 : *
580 : * \param[in] s The string to be checked.
581 : *
582 : * \return true if the string represents "false".
583 : */
584 11 : bool is_false(std::string s)
585 : {
586 11 : return s == "false" || s == "off" || s == "no" || s == "0";
587 : }
588 :
589 :
590 :
591 6 : } // namespace advgetopt
592 : // vim: ts=4 sw=4 et
|