Line data Source code
1 : // Copyright (c) 2006-2024 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/isatty.h>
39 : #include <snapdev/not_used.h>
40 : #include <snapdev/trim_string.h>
41 :
42 :
43 : // cppthread
44 : //
45 : #include <cppthread/guard.h>
46 : #include <cppthread/mutex.h>
47 :
48 :
49 : // C++
50 : //
51 : #include <cstring>
52 : #include <iomanip>
53 : #include <set>
54 : #include <sstream>
55 :
56 :
57 : // C
58 : //
59 : #include <sys/ioctl.h>
60 : #include <sys/stat.h>
61 : #include <unistd.h>
62 :
63 :
64 :
65 :
66 : // last include
67 : //
68 : #include <snapdev/poison.h>
69 :
70 :
71 :
72 : namespace advgetopt
73 : {
74 :
75 :
76 :
77 : namespace
78 : {
79 :
80 :
81 :
82 : /** \brief The configuration file mutex.
83 : *
84 : * This options are generally viewed as read-only global variables. They
85 : * get setup once early on and then used and reused as many times as
86 : * required.
87 : *
88 : * This mutex makes sure that access between multiple thread happens in
89 : * a safe manner.
90 : */
91 : cppthread::mutex * g_mutex;
92 :
93 :
94 :
95 : constexpr char const g_single_quote = '\'';
96 : constexpr char const * g_empty_string = "\"\"";
97 : constexpr char const * g_escaped_single_quotes = "'\\''";
98 : constexpr char const * g_simple_characters = "+-./0123456789=ABCEFGHIJKLMNOPQRSTUVWXYZabcefghijklmnopqrstuvwxyz_";
99 :
100 :
101 :
102 : }
103 : // no name namespace
104 :
105 :
106 :
107 : /** \brief Get a global mutex.
108 : *
109 : * This function returns a global mutex we can use to lock the advgetopt
110 : * whenever multithread functionality is required (i.e. a global is used.)
111 : *
112 : * It is safe to call this function early (i.e. before main was ever
113 : * called.)
114 : *
115 : * Usage:
116 : *
117 : * \code
118 : * cppthread::guard lock(get_global_mutex());
119 : * \endcode
120 : *
121 : * \return A reference to our global mutex.
122 : */
123 12788 : cppthread::mutex & get_global_mutex()
124 : {
125 : {
126 12788 : cppthread::guard lock(*cppthread::g_system_mutex);
127 :
128 12788 : if(g_mutex == nullptr)
129 : {
130 1 : g_mutex = new cppthread::mutex();
131 : }
132 12788 : }
133 :
134 12788 : return *g_mutex;
135 : }
136 :
137 :
138 :
139 : /** \brief Remove single (') or double (") quotes from a string.
140 : *
141 : * If a string starts and ends with the same quotation mark, then it
142 : * gets removed.
143 : *
144 : * If no quotes appear, then the function returns a copy of the input as is.
145 : *
146 : * The \p pairs parameter must have an even size (or the last character
147 : * gets ignored). By default, it is set to the double and single quotes:
148 : *
149 : * \code
150 : * "\"\"''"
151 : * \endcode
152 : *
153 : * To remove square, angle, curly brackets:
154 : *
155 : * \code
156 : * "[]<>{}"
157 : * \endcode
158 : *
159 : * \todo
160 : * Add support for UTF-8 quotes. Right now only quotes of 1 byte will
161 : * work.
162 : *
163 : * \param[in] s The string to unquote.
164 : * \param[in] pairs A list of accepted quotes.
165 : *
166 : * \return The unquoted string.
167 : */
168 1067 : std::string unquote(std::string const & s, std::string const & pairs)
169 : {
170 1067 : if(s.length() >= 2)
171 : {
172 943 : std::string::size_type const max(pairs.length() - 1);
173 2718 : for(std::string::size_type pos(0); pos < max; pos += 2)
174 : {
175 1848 : if(s.front() == pairs[pos + 0]
176 1848 : && s.back() == pairs[pos + 1])
177 : {
178 73 : return s.substr(1, s.length() - 2);
179 : }
180 : }
181 : }
182 :
183 994 : return s;
184 : }
185 :
186 :
187 : /** \brief The converse of unquote.
188 : *
189 : * This function adds quotes around a string.
190 : *
191 : * If you do not define the \p close quotation (i.e. it remains set to the
192 : * NUL character '\0'), then the \p open quotation gets reused as the closing
193 : * quotation.
194 : *
195 : * \param[in] s The string to be quoted.
196 : * \param[in] open The opening quote to quote this string.
197 : * \param[in] close The closing quote to quote this string.
198 : *
199 : * \return The input string quoted with \p quote.
200 : */
201 24 : std::string quote(std::string const & s, char open, char close)
202 : {
203 24 : std::string result;
204 :
205 24 : if(close == '\0')
206 : {
207 14 : close = open;
208 : }
209 :
210 24 : result += open;
211 80 : for(auto const c : s)
212 : {
213 56 : if(c == open
214 51 : || c == close)
215 : {
216 10 : result += '\\';
217 : }
218 56 : result += c;
219 : }
220 24 : result += close;
221 :
222 24 : return result;
223 0 : }
224 :
225 :
226 : /** \brief Convert the `_` found in a string to `-` instead.
227 : *
228 : * Options are saved with `-` instead of `_` so all the standard compare
229 : * functions can be used to find options. This function converts a string
230 : * so all of the `_` charaters get transformed to `-` characters.
231 : *
232 : * Why do we support both?
233 : *
234 : * It is customary to use the `-` in long command line option names.
235 : * For example `--long-form` uses a `-`. (One exception is ffmpeg which
236 : * uses `_` in their long command line option names).
237 : *
238 : * However, the advgetopt library also reads Unix like configuration files
239 : * and parameters in those files are generally expected to use underscores
240 : * (`_`) in their names. For example `email_address = contact@example.com`.
241 : *
242 : * To make it simpler, the advgetopt library accepts both characters and
243 : * decides to view them as being equal. So you can use both forms in both
244 : * situations. The following are equivalent:
245 : *
246 : * \code
247 : * my-command --long-form
248 : * my-command --long_form
249 : * \endcode
250 : *
251 : * This function is used to convert a string to the advgetopt format which
252 : * is to keep only `-` in the names. So if it finds a `_`, it gets
253 : * transformed.
254 : *
255 : * \param[in] s The string to transform.
256 : *
257 : * \return A copy with all `_` transformed to `-`.
258 : */
259 8797 : std::string option_with_dashes(std::string const & s)
260 : {
261 8797 : std::string result;
262 8797 : result.reserve(s.length());
263 96914 : for(auto const & c : s)
264 : {
265 88117 : if(c == '_')
266 : {
267 40 : result += '-';
268 : }
269 : else
270 : {
271 88077 : result += c;
272 : }
273 : }
274 8797 : return result;
275 0 : }
276 :
277 :
278 : /** \brief Converts an option back to using underscores.
279 : *
280 : * When generating some error messages, we like to show underscores if the
281 : * variable comes from a configuration file. In this case we use this function
282 : * to convert the dashes back to underscores and print that in the message.
283 : *
284 : * \param[in] s The string to be converted.
285 : *
286 : * \return A copy of the string with `-` converted to `_`.
287 : */
288 5 : std::string option_with_underscores(std::string const & s)
289 : {
290 5 : std::string result;
291 5 : result.reserve(s.length());
292 33 : for(auto const & c : s)
293 : {
294 28 : if(c == '-')
295 : {
296 1 : result += '_';
297 : }
298 : else
299 : {
300 27 : result += c;
301 : }
302 : }
303 5 : return result;
304 0 : }
305 :
306 :
307 : /** \brief Split a string in sub-strings separated by \p separators.
308 : *
309 : * This function searches for any of the \p separators in \p str and
310 : * split at those locations.
311 : *
312 : * For example, to split a comma separated list of strings, use the
313 : * following:
314 : *
315 : * \code
316 : * string_list_t result;
317 : * option_info::split_string(string_to_split, result, {","});
318 : * \endcode
319 : *
320 : * If `string_to_split` is set to "a, b, c", then the `result` vector
321 : * will have three strings as a result: `a`, `b`, and `c`. Note that
322 : * the function automatically trims all strings and it never keeps
323 : * empty strings. So two separators one after another is accepted and
324 : * no empty string results.
325 : *
326 : * The trimming happens after the split occurs. This allows for the
327 : * list of separators to include spaces as separators.
328 : *
329 : * The function does not clear the result vector. This allows you to
330 : * call this function multiple times with various strings and the
331 : * results will be cumulated.
332 : *
333 : * \note
334 : * This function is a static so it can be used from anywhere to split
335 : * strings as required. You do not need to have an option_info instance.
336 : *
337 : * \todo
338 : * See to fix the fact that `a"b"c` becomes `{"a", "b", "c"}` when
339 : * there are not separators between `a`, `"b"`, and `c`. To the minimum
340 : * we may want to generate an error when such is found (i.e. when a
341 : * quote is found and `start < pos` is true.
342 : *
343 : * \param[in] str The string to split.
344 : * \param[in] result The vector where the split strings are saved.
345 : * \param[in] separators The vector of strings used as separators.
346 : */
347 238 : void split_string(std::string const & str
348 : , string_list_t & result
349 : , string_list_t const & separators)
350 : {
351 238 : std::string::size_type pos(0);
352 238 : std::string::size_type start(0);
353 10383 : while(pos < str.length())
354 : {
355 10145 : if(str[pos] == '\'' || str[pos] == '"')
356 : {
357 9 : if(start < pos)
358 : {
359 12 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
360 4 : if(!v.empty())
361 : {
362 4 : result.push_back(v);
363 : }
364 4 : start = pos;
365 4 : }
366 :
367 : // quoted parameters are handled without the separators
368 : //
369 9 : char const quote(str[pos]);
370 107 : for(++pos; pos < str.length() && str[pos] != quote; ++pos);
371 :
372 9 : std::string const v(str.substr(start + 1, pos - (start + 1)));
373 9 : if(!v.empty())
374 : {
375 6 : result.push_back(v);
376 : }
377 9 : if(pos < str.length())
378 : {
379 : // skip the closing quote
380 : //
381 7 : ++pos;
382 : }
383 9 : start = pos;
384 9 : }
385 : else
386 : {
387 10136 : bool found(false);
388 19643 : for(auto const & sep : separators)
389 : {
390 10153 : if(str.length() - pos >= sep.length())
391 : {
392 10116 : if(str.compare(pos, sep.length(), sep) == 0)
393 : {
394 : // match! cut here
395 : //
396 646 : if(start < pos)
397 : {
398 1878 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
399 626 : if(!v.empty())
400 : {
401 626 : result.push_back(v);
402 : }
403 626 : }
404 646 : pos += sep.length();
405 646 : start = pos;
406 646 : found = true;
407 646 : break;
408 : }
409 : }
410 : }
411 :
412 10136 : if(!found)
413 : {
414 9490 : ++pos;
415 : }
416 : }
417 : }
418 :
419 238 : if(start < pos)
420 : {
421 702 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
422 234 : if(!v.empty())
423 : {
424 234 : result.push_back(v);
425 : }
426 234 : }
427 238 : }
428 :
429 :
430 : /** \brief Insert the group (or project) name in the filename.
431 : *
432 : * This function inserts the name of the group in the specified full path
433 : * filename. It gets added right before the basename. So for example you
434 : * have a path such as:
435 : *
436 : * /etc/snapwebsites/advgetopt.conf
437 : *
438 : * and a group name such as:
439 : *
440 : * adventure
441 : *
442 : * The resulting path is:
443 : *
444 : * /etc/snapwebsites/adventure.d/##-advgetopt.conf
445 : *
446 : * where the '##' is a number from 00 to 99. If none of those files exists,
447 : * the default (50) is used if \p add_default_on_empty is true.
448 : *
449 : * Notice that the function adds a ".d" as well.
450 : *
451 : * If the group name is empty or null, then the project name is used. If
452 : * both are empty, then nothing happens (the function returns an empty list).
453 : *
454 : * \exception getopt_root_filename
455 : * The \p filename parameter cannot be a file in the root directory.
456 : *
457 : * \param[in] filename The filename where the project name gets injected.
458 : * \param[in] group_name The name of the group to inject in the filename.
459 : * \param[in] project_name The name of the project to inject in the filename.
460 : * \param[in] add_default_on_empty Whether the add the default if no files
461 : * exist.
462 : *
463 : * \return The list of filenames or an empty list if no group or project name
464 : * or filename were specified.
465 : */
466 730 : string_list_t insert_group_name(
467 : std::string const & filename
468 : , char const * group_name
469 : , char const * project_name
470 : , bool add_default_on_empty)
471 : {
472 730 : if(filename.empty())
473 : {
474 5 : return string_list_t();
475 : }
476 :
477 725 : std::string name;
478 725 : if(group_name == nullptr
479 128 : || *group_name == '\0')
480 : {
481 601 : if(project_name == nullptr
482 599 : || *project_name == '\0')
483 : {
484 4 : return string_list_t();
485 : }
486 597 : name = project_name;
487 : }
488 : else
489 : {
490 124 : name = group_name;
491 : }
492 :
493 721 : std::string pattern;
494 721 : std::string::size_type const pos(filename.find_last_of('/'));
495 721 : if(pos == 0)
496 : {
497 2 : throw getopt_root_filename(
498 : "filename \""
499 2 : + filename
500 5 : + "\" last slash (/) is at the start, which is not allowed.");
501 : }
502 720 : if(pos != std::string::npos
503 496 : && pos > 0)
504 : {
505 1488 : pattern = filename.substr(0, pos + 1)
506 1488 : + name
507 1488 : + ".d/[0-9][0-9]-"
508 1488 : + filename.substr(pos + 1);
509 : }
510 : else
511 : {
512 224 : pattern = name
513 448 : + ".d/[0-9][0-9]-"
514 448 : + filename;
515 : }
516 :
517 : // we use an std::set so the resulting list is sorted
518 : //
519 720 : snapdev::glob_to_list<std::set<std::string>> glob;
520 :
521 : // the glob() function is not thread safe
522 : {
523 720 : cppthread::guard lock(get_global_mutex());
524 720 : snapdev::NOT_USED(glob.read_path<snapdev::glob_to_list_flag_t::GLOB_FLAG_IGNORE_ERRORS>(pattern));
525 720 : }
526 :
527 : // we add the default name if none other exists
528 : //
529 720 : if(add_default_on_empty
530 720 : && glob.empty())
531 : {
532 698 : glob.insert(default_group_name(
533 : filename
534 : , group_name
535 : , project_name));
536 : }
537 :
538 720 : return string_list_t(glob.begin(), glob.end());
539 726 : }
540 :
541 :
542 : /** \brief Generate the default filename (the ".../50-...")
543 : *
544 : * This function generates the default filename as the insert_group_name()
545 : * expects to find in the configuration sub-directory.
546 : *
547 : * The name is formed as follow:
548 : *
549 : * <path> / <directory> ".d" / <priority> "-" <basename>
550 : *
551 : * Where `<path>` is the path found in \p filename. If no path is defined in
552 : * \p filename, then the `<path> /` part is not prepended:
553 : *
554 : * <directory> ".d" / <priority> "-" <basename>
555 : *
556 : * Where `<directory>` is the \p group_name if defined, otherwise it uses
557 : * the \p project_name. This is why if neither is defined, then the function
558 : * immediately returns an empty string.
559 : *
560 : * Where `<priority>` is a number from 0 to 99 inclusive. This is used to
561 : * sort the files before processing them. File with lower priorities are
562 : * loaded first. Parameters found in files with higher priorities overwrite
563 : * the values of parameters found in files with lower priorities.
564 : *
565 : * Where `<basename>` is the end of \p filename, the part after the last
566 : * slash (`/`). If \p filename is not empty and it does not include a slash
567 : * then the entire \p filename is taken as the `<basename>`. Note that
568 : * \p filename is expected to include an extension such as `.conf`. The
569 : * extension is not modified in any way.
570 : *
571 : * Since the result is not viable when \p filename is empty, the function
572 : * immediately returns an empty string in that situation.
573 : *
574 : * \exception getopt_root_filename
575 : * The \p filename parameter cannot be a file in the root directory.
576 : *
577 : * \param[in] filename The filename where the project name gets injected.
578 : * \param[in] group_name The name of the group to inject in the filename.
579 : * \param[in] project_name The name of the project to inject in the filename.
580 : * \param[in] priority The priority of the new file (0 to 99).
581 : *
582 : * \return The default filenames or an empty list if no group or project
583 : * or file name were specified.
584 : */
585 879 : std::string default_group_name(
586 : std::string const & filename
587 : , char const * group_name
588 : , char const * project_name
589 : , int priority)
590 : {
591 879 : if(priority < 0 || priority >= 100)
592 : {
593 80 : throw getopt_invalid_parameter(
594 : "priority must be a number between 0 and 99 inclusive; "
595 80 : + std::to_string(priority)
596 200 : + " is invalid.");
597 : }
598 :
599 839 : if(filename.empty())
600 : {
601 5 : return std::string();
602 : }
603 :
604 834 : char const * name(nullptr);
605 834 : if(group_name == nullptr
606 239 : || *group_name == '\0')
607 : {
608 601 : if(project_name == nullptr
609 599 : || *project_name == '\0')
610 : {
611 4 : return std::string();
612 : }
613 597 : name = project_name;
614 : }
615 : else
616 : {
617 233 : name = group_name;
618 : }
619 :
620 830 : std::string::size_type const pos(filename.find_last_of('/'));
621 830 : if(pos == 0)
622 : {
623 1 : throw getopt_root_filename("filename \"" + filename + "\" starts with a slash (/), which is not allowed.");
624 : }
625 :
626 829 : std::string result;
627 829 : result.reserve(filename.length() + strlen(name) + 6);
628 829 : if(pos != std::string::npos)
629 : {
630 590 : result = filename.substr(0, pos + 1);
631 : }
632 829 : result += name;
633 829 : result += ".d/";
634 829 : if(priority < 10)
635 : {
636 10 : result += '0';
637 : }
638 829 : result += std::to_string(priority);
639 829 : result += '-';
640 829 : if(pos == std::string::npos)
641 : {
642 239 : result += filename;
643 : }
644 : else
645 : {
646 590 : result += filename.substr(pos + 1);
647 : }
648 :
649 829 : return result;
650 829 : }
651 :
652 :
653 : /** \brief Replace a starting `~/...` with the contents of the \$HOME variable.
654 : *
655 : * This function checks the beginning of \p filename. If it starts with `'~/'`
656 : * then it replaces the `'~'` character with the contents of the \$HOME
657 : * environment variable.
658 : *
659 : * If \p filename is just `"~"`, then the function returns the contents of
660 : * the \$HOME environment variable by itself.
661 : *
662 : * If somehow the \$HOME environment variable is empty, the function does
663 : * nothing.
664 : *
665 : * \todo
666 : * Add support for "~<user name>/..." so that way a service could use its
667 : * own home folder even when run from a different user (a.k.a. root). This
668 : * requires that we load the user database and get the home folder from that
669 : * data.
670 : *
671 : * \param[in] filename The filename to check for a tilde (~).
672 : *
673 : * \return The input as is unless the \$HOME path can be prepended to replace
674 : * the tilde (~) character.
675 : */
676 758 : std::string handle_user_directory(std::string const & filename)
677 : {
678 758 : if(!filename.empty()
679 758 : && filename[0] == '~'
680 1516 : && (filename.length() == 1 || filename[1] == '/'))
681 : {
682 53 : char const * const home(getenv("HOME"));
683 53 : if(home != nullptr
684 49 : && *home != '\0')
685 : {
686 94 : return home + filename.substr(1);
687 : }
688 : }
689 :
690 711 : return filename;
691 : }
692 :
693 :
694 : /** \brief Check whether a value represents "true".
695 : *
696 : * This function checks a string to see whether it is one of:
697 : *
698 : * * "true"
699 : * * "on"
700 : * * "yes"
701 : * * "1"
702 : *
703 : * If so, then the function returns true.
704 : *
705 : * \param[in] s The string to be checked.
706 : *
707 : * \return true if the string represents "true".
708 : */
709 9 : bool is_true(std::string s)
710 : {
711 9 : return s == "true" || s == "on" || s == "yes" | s == "1";
712 : }
713 :
714 :
715 : /** \brief Check whether a value represents "false".
716 : *
717 : * This function checks a string to see whether it is one of:
718 : *
719 : * * "false"
720 : * * "off"
721 : * * "no"
722 : * * "0"
723 : *
724 : * If so, then the function returns true.
725 : *
726 : * \param[in] s The string to be checked.
727 : *
728 : * \return true if the string represents "false".
729 : */
730 11 : bool is_false(std::string s)
731 : {
732 11 : return s == "false" || s == "off" || s == "no" || s == "0";
733 : }
734 :
735 :
736 : /** \brief Retrieve the width of one line in your console.
737 : *
738 : * This function retrieves the width of the console in number of characters.
739 : *
740 : * If the process is not connected to a TTY, then the function returns 80.
741 : *
742 : * If the width is less than 40, the function returns 40.
743 : *
744 : * \return The width of the console screen.
745 : */
746 299 : size_t get_screen_width()
747 : {
748 299 : std::int64_t cols(80);
749 :
750 299 : if(isatty(STDOUT_FILENO))
751 : {
752 : // LCOV_EXCL_START
753 : // when running coverage, the output is redirected for logging purposes
754 : // which means that isatty() returns false -- so at this time I just
755 : // exclude those since they are unreachable from my standard Unit Tests
756 : //
757 : winsize w;
758 : if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) != -1)
759 : {
760 : cols = std::max(static_cast<unsigned short>(40), w.ws_col);
761 : }
762 : // LCOV_EXCL_STOP
763 : }
764 :
765 299 : return cols;
766 : }
767 :
768 :
769 : /** \brief Breakup a string on multiple lines.
770 : *
771 : * This function breaks up the specified \p line of text in one or more
772 : * strings to fit your output.
773 : *
774 : * The \p line_width represents the maximum number of characters that get
775 : * printed in a row.
776 : *
777 : * The \p option_width parameter is the number of characters in the left
778 : * margin. When dealing with a very long argument, this width is 3 characters.
779 : * When dealing with the help itself, it is expected to be around 30.
780 : *
781 : * \note
782 : * This function always makes sure that the resulting string ends with
783 : * a newline character unless the input \p line string is empty.
784 : *
785 : * \param[in] line The line to breakup.
786 : * \param[in] option_width The number of characters in the left margin.
787 : * \param[in] line_width The total number of characters in the output.
788 : *
789 : * \return The broken up line as required.
790 : */
791 558 : std::string breakup_line(
792 : std::string line
793 : , size_t const option_width
794 : , size_t const line_width)
795 : {
796 558 : std::stringstream ss;
797 :
798 558 : size_t const width(line_width - option_width);
799 :
800 : // TODO: once we have C++17, avoid substr() using std::string_view instead
801 : //
802 : for(;;)
803 : {
804 1365 : std::string l;
805 1365 : std::string::size_type const nl(line.find('\n'));
806 1365 : if(nl != std::string::npos
807 447 : && nl < width)
808 : {
809 301 : l = line.substr(0, nl);
810 301 : line = line.substr(nl + 1);
811 : }
812 1064 : else if(line.size() <= width)
813 : {
814 558 : break;
815 : }
816 506 : else if(std::isspace(line[width]))
817 : {
818 : // special case when the space is right at the edge
819 : //
820 41 : l = line.substr(0, width);
821 41 : size_t pos(width);
822 : do
823 : {
824 43 : ++pos;
825 : }
826 43 : while(std::isspace(line[pos]));
827 41 : line = line.substr(pos);
828 : }
829 : else
830 : {
831 : // search for the last space before the edge of the screen
832 : //
833 465 : std::string::size_type pos(line.find_last_of(' ', width));
834 465 : if(pos == std::string::npos)
835 : {
836 : // no space found, cut right at the edge...
837 : // (this should be really rare)
838 : //
839 84 : l = line.substr(0, width);
840 84 : line = line.substr(width);
841 : }
842 : else
843 : {
844 : // we found a space, write everything up to that space
845 : //
846 381 : l = line.substr(0, pos);
847 :
848 : // remove additional spaces from the start of the next line
849 : do // LCOV_EXCL_LINE
850 : {
851 381 : ++pos;
852 : }
853 381 : while(std::isspace(line[pos]));
854 381 : line = line.substr(pos);
855 : }
856 : }
857 :
858 807 : ss << l
859 807 : << std::endl;
860 :
861 : // more to print? if so we need the indentation
862 : //
863 807 : if(!line.empty()
864 807 : && option_width > 0)
865 : {
866 243 : ss << std::setw(option_width) << " ";
867 : }
868 2172 : }
869 :
870 : // some leftover?
871 : //
872 558 : if(!line.empty())
873 : {
874 532 : ss << line << std::endl;
875 : }
876 :
877 1116 : return ss.str();
878 558 : }
879 :
880 :
881 : /** \brief Format a help string to make it fit on a given width.
882 : *
883 : * This function properly wraps a set of help strings so they fit in
884 : * your console. The width has to be given by you at the moment.
885 : *
886 : * The function takes two strings, the argument with it's options
887 : * and the actual help string for that argument. If the argument
888 : * is short enough, it will appear on the first line with the
889 : * first line of help. If not, then one whole line is reserved
890 : * just for the argument and the help starts on the next line.
891 : *
892 : * \param[in] argument The option name with -- and arguments.
893 : * \param[in] help The help string for this argument.
894 : * \param[in] option_width Number of characters reserved for the option.
895 : * \param[in] line_width The maximum number of characters to display in width.
896 : *
897 : * \return A help string formatted for display.
898 : */
899 314 : std::string format_usage_string(
900 : std::string const & argument
901 : , std::string const & help
902 : , size_t const option_width
903 : , size_t const line_width)
904 : {
905 314 : std::stringstream ss;
906 :
907 314 : ss << " ";
908 :
909 314 : if(argument.size() < option_width - 3)
910 : {
911 : // enough space on a single line
912 : //
913 : ss << argument
914 500 : << std::setw(option_width - 3 - argument.size())
915 250 : << " ";
916 : }
917 64 : else if(argument.size() >= line_width - 4)
918 : {
919 : // argument too long for even one line on the screen!?
920 : // call the function to break it up with indentation of 3
921 : //
922 2 : ss << breakup_line(argument, 3, line_width);
923 :
924 2 : if(!help.empty()
925 2 : && option_width > 0)
926 : {
927 2 : ss << std::setw(option_width) << " ";
928 : }
929 : }
930 : else
931 : {
932 : // argument too long for the help to follow immediately
933 : //
934 62 : ss << argument
935 62 : << std::endl
936 : << std::setw(option_width)
937 62 : << " ";
938 : }
939 :
940 314 : ss << breakup_line(help, option_width, line_width);
941 :
942 628 : return ss.str();
943 314 : }
944 :
945 :
946 : /** \brief Escape special characters from a shell argument.
947 : *
948 : * This function goes through the supplied argument. If it includes one
949 : * or more character other than `[-+0-9A-Za-z_]`, then it gets \em escaped.
950 : * This means we add single quotes at the start and end, and escape any
951 : * single quote within the argument.
952 : *
953 : * So the function may return the input string as is.
954 : *
955 : * \param[in] arg The argument to escape.
956 : *
957 : * \return The escaped argument.
958 : */
959 110 : std::string escape_shell_argument(std::string const & arg)
960 : {
961 110 : if(arg.empty())
962 : {
963 1 : return std::string(g_empty_string);
964 : }
965 :
966 109 : std::string::size_type const pos(arg.find_first_not_of(g_simple_characters));
967 109 : if(pos == std::string::npos)
968 : {
969 106 : return arg;
970 : }
971 :
972 3 : std::string result;
973 :
974 3 : result += g_single_quote;
975 3 : std::string::size_type p1(0);
976 6 : while(p1 < arg.length())
977 : {
978 5 : std::string::size_type const p2(arg.find('\'', p1));
979 5 : if(p2 == std::string::npos)
980 : {
981 2 : result += arg.substr(p1);
982 2 : break;
983 : }
984 3 : result += arg.substr(p1, p2 - p1);
985 3 : result += g_escaped_single_quotes;
986 3 : p1 = p2 + 1; // skip the '
987 : }
988 3 : result += g_single_quote;
989 :
990 3 : return result;
991 3 : }
992 :
993 :
994 : /** \brief Generate a string describing whether we're using the sanitizer.
995 : *
996 : * This function determines whether this library was compiled with the
997 : * sanitizer extension. If so, then it will return detail about which
998 : * feature was compiled in.
999 : *
1000 : * If no sanitizer options were compiled in, then it returns a
1001 : * message saying so.
1002 : *
1003 : * \return A string with details about the sanitizer.
1004 : */
1005 2 : std::string sanitizer_details()
1006 : {
1007 2 : std::string result;
1008 : #if defined(__SANITIZE_ADDRESS__) || defined(__SANITIZE_THREAD__)
1009 : #if defined(__SANITIZE_ADDRESS__)
1010 2 : result += "The address sanitizer is compiled in.\n";
1011 : #endif
1012 : #if defined(__SANITIZE_THREAD__)
1013 : result += "The thread sanitizer is compiled in.\n";
1014 : #endif
1015 : #else
1016 : result += "The address and thread sanitizers are not compiled in.\n";
1017 : #endif
1018 2 : return result;
1019 0 : }
1020 :
1021 :
1022 : /** \brief Retrieve the height of your console.
1023 : *
1024 : * This function retrieves the height of the console in number of characters.
1025 : * This is also called the number of rows.
1026 : *
1027 : * If the process is not connected to a TTY, then the function returns 25.
1028 : *
1029 : * If the height is less than 2, the function returns 2.
1030 : *
1031 : * \note
1032 : * The get_screen_width() and get_screen_height() should be combined.
1033 : *
1034 : * \return The width of the console screen.
1035 : */
1036 : // LCOV_EXCL_START
1037 : size_t get_screen_height()
1038 : {
1039 : std::int64_t rows(25);
1040 :
1041 : if(isatty(STDOUT_FILENO))
1042 : {
1043 : // when running coverage, the output is redirected for logging purposes
1044 : // which means that isatty() returns false -- so at this time I just
1045 : // exclude those since they are unreachable from my standard Unit Tests
1046 : //
1047 : winsize w;
1048 : if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) != -1)
1049 : {
1050 : rows = std::max(static_cast<unsigned short>(2), w.ws_row);
1051 : }
1052 : }
1053 :
1054 : return rows;
1055 : }
1056 : // LCOV_EXCL_STOP
1057 :
1058 :
1059 : /** \brief Print out a string to the console or use less.
1060 : *
1061 : * If the \p data string to be output is too large for the screen (too
1062 : * many lines; we assume the width was already "fixed") then use less
1063 : * to show the data. If less is not available, use more. If neither
1064 : * is available, fall back to printing everything at once.
1065 : *
1066 : * \param[in,out] out The output stream where the data has to be written.
1067 : * \param[in] data The data to be written to stream.
1068 : */
1069 9 : void less(std::basic_ostream<char> & out, std::string const & data)
1070 : {
1071 9 : if(snapdev::isatty(out))
1072 : {
1073 : // LCOV_EXCL_START
1074 : auto const lines(std::count(data.begin(), data.end(), '\n'));
1075 : size_t const height(get_screen_height());
1076 : if(lines > static_cast<std::remove_const_t<decltype(lines)>>(height))
1077 : {
1078 : struct stat s;
1079 : if(stat("/bin/less", &s) == 0)
1080 : {
1081 : FILE * f(popen("/bin/less", "w"));
1082 : if(f != nullptr)
1083 : {
1084 : fwrite(data.c_str(), sizeof(char), data.length(), f);
1085 : pclose(f);
1086 : return;
1087 : }
1088 : }
1089 : else if(stat("/bin/more", &s) == 0)
1090 : {
1091 : FILE * f(popen("/bin/more", "w"));
1092 : if(f != nullptr)
1093 : {
1094 : fwrite(data.c_str(), sizeof(char), data.length(), f);
1095 : pclose(f);
1096 : return;
1097 : }
1098 : }
1099 : }
1100 : // LCOV_EXCL_STOP
1101 : }
1102 :
1103 : // fallback, just print everything to the console as is
1104 : //
1105 9 : out << data << std::endl;
1106 : }
1107 :
1108 :
1109 :
1110 : } // namespace advgetopt
1111 : // vim: ts=4 sw=4 et
|