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/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 8498 : cppthread::mutex & get_global_mutex()
124 : {
125 : {
126 16996 : cppthread::guard lock(*cppthread::g_system_mutex);
127 :
128 8498 : if(g_mutex == nullptr)
129 : {
130 1 : g_mutex = new cppthread::mutex();
131 : }
132 : }
133 :
134 8498 : 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 1059 : std::string unquote(std::string const & s, std::string const & pairs)
169 : {
170 1059 : if(s.length() >= 2)
171 : {
172 935 : std::string::size_type const max(pairs.length() - 1);
173 2696 : for(std::string::size_type pos(0); pos < max; pos += 2)
174 : {
175 3666 : if(s.front() == pairs[pos + 0]
176 1833 : && s.back() == pairs[pos + 1])
177 : {
178 72 : return s.substr(1, s.length() - 2);
179 : }
180 : }
181 : }
182 :
183 987 : return s;
184 : }
185 :
186 :
187 : /** \brief The converse of unquote.
188 : *
189 : * This function adds quotes around a string.
190 : *
191 : * \param[in] s The string to be quoted.
192 : * \param[in] q The quotes to use to quote this string.
193 : *
194 : * \return The input string quoted with \p quote.
195 : */
196 0 : std::string quote(std::string s, char q)
197 : {
198 0 : std::string result;
199 :
200 0 : result += q;
201 0 : for(auto const c : s)
202 : {
203 0 : if(c == q)
204 : {
205 0 : result += '\\';
206 : }
207 0 : result += c;
208 : }
209 0 : result += q;
210 :
211 0 : return result;
212 : }
213 :
214 :
215 : /** \brief Split a string in sub-strings separated by \p separators.
216 : *
217 : * This function searches for any of the \p separators in \p str and
218 : * split at those locations.
219 : *
220 : * For example, to split a comma separated list of strings, use the
221 : * following:
222 : *
223 : * \code
224 : * string_list_t result;
225 : * option_info::split_string(string_to_split, result, {","});
226 : * \endcode
227 : *
228 : * If `string_to_split` is set to "a, b, c", then the `result` vector
229 : * will have three strings as a result: `a`, `b`, and `c`. Note that
230 : * the function automatically trims all strings and it never keeps
231 : * empty strings. So two separators one after another is accepted and
232 : * no empty string results.
233 : *
234 : * The trimming happens after the split occurs. This allows for the
235 : * list of separators to include spaces as separators.
236 : *
237 : * The function does not clear the result vector. This allows you to
238 : * call this function multiple times with various strings and the
239 : * results will be cumulated.
240 : *
241 : * \note
242 : * This function is a static so it can be used from anywhere to split
243 : * strings as required. You do not need to have an option_info instance.
244 : *
245 : * \todo
246 : * See to fix the fact that `a"b"c` becomes `{"a", "b", "c"}` when
247 : * there are not separators between `a`, `"b"`, and `c`. To the minimum
248 : * we may want to generate an error when such is found (i.e. when a
249 : * quote is found and `start < pos` is true.
250 : *
251 : * \param[in] str The string to split.
252 : * \param[in] result The vector where the split strings are saved.
253 : * \param[in] separators The vector of strings used as separators.
254 : */
255 201 : void split_string(std::string const & str
256 : , string_list_t & result
257 : , string_list_t const & separators)
258 : {
259 201 : std::string::size_type pos(0);
260 201 : std::string::size_type start(0);
261 19385 : while(pos < str.length())
262 : {
263 9592 : if(str[pos] == '\'' || str[pos] == '"')
264 : {
265 9 : if(start < pos)
266 : {
267 8 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
268 4 : if(!v.empty())
269 : {
270 4 : result.push_back(v);
271 : }
272 4 : start = pos;
273 : }
274 :
275 : // quoted parameters are handled without the separators
276 : //
277 9 : char const quote(str[pos]);
278 9 : for(++pos; pos < str.length() && str[pos] != quote; ++pos);
279 :
280 18 : std::string const v(str.substr(start + 1, pos - (start + 1)));
281 9 : if(!v.empty())
282 : {
283 6 : result.push_back(v);
284 : }
285 9 : if(pos < str.length())
286 : {
287 : // skip the closing quote
288 : //
289 7 : ++pos;
290 : }
291 9 : start = pos;
292 : }
293 : else
294 : {
295 9583 : bool found(false);
296 18578 : for(auto const & sep : separators)
297 : {
298 9600 : if(str.length() - pos >= sep.length())
299 : {
300 9600 : if(str.compare(pos, sep.length(), sep) == 0)
301 : {
302 : // match! cut here
303 : //
304 605 : if(start < pos)
305 : {
306 1170 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
307 585 : if(!v.empty())
308 : {
309 585 : result.push_back(v);
310 : }
311 : }
312 605 : pos += sep.length();
313 605 : start = pos;
314 605 : found = true;
315 605 : break;
316 : }
317 : }
318 : }
319 :
320 9583 : if(!found)
321 : {
322 8978 : ++pos;
323 : }
324 : }
325 : }
326 :
327 201 : if(start < pos)
328 : {
329 394 : std::string const v(snapdev::trim_string(str.substr(start, pos - start)));
330 197 : if(!v.empty())
331 : {
332 197 : result.push_back(v);
333 : }
334 : }
335 201 : }
336 :
337 :
338 : /** \brief Insert the group (or project) name in the filename.
339 : *
340 : * This function inserts the name of the group in the specified full path
341 : * filename. It gets added right before the basename. So for example you
342 : * have a path such as:
343 : *
344 : * /etc/snapwebsites/advgetopt.conf
345 : *
346 : * and a group name such as:
347 : *
348 : * adventure
349 : *
350 : * The resulting path is:
351 : *
352 : * /etc/snapwebsites/adventure.d/advgetopt.conf
353 : *
354 : * Notice that the function adds a ".d" as well.
355 : *
356 : * If the group name is empty or null, then the project name is used. If
357 : * both are empty, then nothing happens (the function returns an empty list).
358 : *
359 : * \exception getopt_root_filename
360 : * The \p filename parameter cannot be a file in the root directory.
361 : *
362 : * \param[in] filename The filename where the project name gets injected.
363 : * \param[in] group_name The name of the group to inject in the filename.
364 : * \param[in] project_name The name of the project to inject in the filename.
365 : *
366 : * \return The list of filenames or an empty list if no group or project name
367 : * or filename were specified.
368 : */
369 536 : string_list_t insert_group_name(
370 : std::string const & filename
371 : , char const * group_name
372 : , char const * project_name)
373 : {
374 536 : if(filename.empty())
375 : {
376 5 : return string_list_t();
377 : }
378 :
379 1062 : std::string name;
380 531 : if(group_name == nullptr
381 163 : || *group_name == '\0')
382 : {
383 372 : if(project_name == nullptr
384 370 : || *project_name == '\0')
385 : {
386 4 : return string_list_t();
387 : }
388 368 : name = project_name;
389 : }
390 : else
391 : {
392 159 : name = group_name;
393 : }
394 :
395 1054 : std::string pattern;
396 527 : std::string::size_type const pos(filename.find_last_of('/'));
397 527 : if(pos == 0)
398 : {
399 1 : throw getopt_root_filename("filename \"" + filename + "\" last slash (/) is at the start, which is not allowed.");
400 : }
401 526 : if(pos != std::string::npos
402 250 : && pos > 0)
403 : {
404 1250 : pattern = filename.substr(0, pos + 1)
405 750 : + name
406 750 : + ".d/[0-9][0-9]-"
407 1000 : + filename.substr(pos + 1);
408 : }
409 : else
410 : {
411 552 : pattern = name
412 552 : + (".d/[0-9][0-9]-" + filename);
413 : }
414 :
415 : // we use an std::set so the resulting list is sorted
416 : //
417 1052 : snapdev::glob_to_list<std::set<std::string>> glob;
418 :
419 : // the glob() function is not thread safe
420 : {
421 1052 : cppthread::guard lock(get_global_mutex());
422 526 : snapdev::NOT_USED(glob.read_path<snapdev::glob_to_list_flag_t::GLOB_FLAG_IGNORE_ERRORS>(pattern));
423 : }
424 :
425 : // we add the default name if none other exists
426 : //
427 526 : if(glob.empty())
428 : {
429 504 : glob.insert(default_group_name(
430 : filename
431 : , group_name
432 : , project_name));
433 : }
434 :
435 526 : return string_list_t(glob.begin(), glob.end());
436 : }
437 :
438 :
439 : /** \brief Generate the default filename (the ".../50-...")
440 : *
441 : * This function generates the default filename as the insert_group_name()
442 : * expects to find in the configuration sub-directory.
443 : *
444 : * The name is formed as follow:
445 : *
446 : * <path> / <directory> ".d" / <priority> "-" <basename>
447 : *
448 : * Where `<path>` is the path found in \p filename. If no path is defined in
449 : * \p filename, then the `<path> /` part is not prepended:
450 : *
451 : * <directory> ".d" / <priority> "-" <basename>
452 : *
453 : * Where `<directory>` is the \p group_name if defined, otherwise it uses
454 : * the \p project_name. This is why if neither is defined, then the function
455 : * immediately returns an empty string.
456 : *
457 : * Where `<priority>` is a number from 0 to 99 inclusive. This is used to
458 : * sort the files before processing them. File with lower priorities are
459 : * loaded first. Parameters found in files with higher priorities overwrite
460 : * the values of parameters found in files with lower priorities.
461 : *
462 : * Where `<basename>` is the end of \p filename, the part after the last
463 : * slash (`/`). If \p filename is not empty and it does not include a slash
464 : * then the entire \p filename is taken as the `<basename>`. Note that
465 : * \p filename is expected to include an extension such as `.conf`. The
466 : * extension is not modified in any way.
467 : *
468 : * Since the result is not viable when \p filename is empty, the function
469 : * immediately returns an empty string in that situation.
470 : *
471 : * \exception getopt_root_filename
472 : * The \p filename parameter cannot be a file in the root directory.
473 : *
474 : * \param[in] filename The filename where the project name gets injected.
475 : * \param[in] group_name The name of the group to inject in the filename.
476 : * \param[in] project_name The name of the project to inject in the filename.
477 : * \param[in] priority The priority of the new file (0 to 99).
478 : *
479 : * \return The default filenames or an empty list if no group or project
480 : * or file name were specified.
481 : */
482 666 : std::string default_group_name(
483 : std::string const & filename
484 : , char const * group_name
485 : , char const * project_name
486 : , int priority)
487 : {
488 666 : if(priority < 0 || priority >= 100)
489 : {
490 : throw getopt_invalid_parameter(
491 : "priority must be a number between 0 and 99 inclusive; "
492 80 : + std::to_string(priority)
493 120 : + " is invalid.");
494 : }
495 :
496 626 : if(filename.empty())
497 : {
498 5 : return std::string();
499 : }
500 :
501 621 : char const * name(nullptr);
502 621 : if(group_name == nullptr
503 269 : || *group_name == '\0')
504 : {
505 358 : if(project_name == nullptr
506 356 : || *project_name == '\0')
507 : {
508 4 : return std::string();
509 : }
510 354 : name = project_name;
511 : }
512 : else
513 : {
514 263 : name = group_name;
515 : }
516 :
517 617 : std::string::size_type const pos(filename.find_last_of('/'));
518 617 : if(pos == 0)
519 : {
520 1 : throw getopt_root_filename("filename \"" + filename + "\" starts with a slash (/), which is not allowed.");
521 : }
522 :
523 1232 : std::string result;
524 616 : result.reserve(filename.length() + strlen(name) + 6);
525 616 : if(pos != std::string::npos)
526 : {
527 338 : result = filename.substr(0, pos + 1);
528 : }
529 616 : result += name;
530 616 : result += ".d/";
531 616 : if(priority < 10)
532 : {
533 10 : result += '0';
534 : }
535 616 : result += std::to_string(priority);
536 616 : result += '-';
537 616 : if(pos == std::string::npos)
538 : {
539 278 : result += filename;
540 : }
541 : else
542 : {
543 338 : result += filename.substr(pos + 1);
544 : }
545 :
546 616 : return result;
547 : }
548 :
549 :
550 : /** \brief Replace a starting `~/...` with the contents of the \$HOME variable.
551 : *
552 : * This function checks the beginning of \p filename. If it starts with `'~/'`
553 : * then it replaces the `'~'` character with the contents of the \$HOME
554 : * environment variable.
555 : *
556 : * If \p filename is just `"~"`, then the function returns the contents of
557 : * the \$HOME environment variable by itself.
558 : *
559 : * If somehow the \$HOME environment variable is empty, the function does
560 : * nothing.
561 : *
562 : * \todo
563 : * Add support for "~<user name>/..." so that way a service could use its
564 : * own home folder even when run from a different user (a.k.a. root). This
565 : * requires that we load the user database and get the home folder from that
566 : * data.
567 : *
568 : * \param[in] filename The filename to check for a tilde (~).
569 : *
570 : * \return The input as is unless the \$HOME path can be prepended to replace
571 : * the tilde (~) character.
572 : */
573 574 : std::string handle_user_directory(std::string const & filename)
574 : {
575 1148 : if(!filename.empty()
576 574 : && filename[0] == '~'
577 637 : && (filename.length() == 1 || filename[1] == '/'))
578 : {
579 63 : char const * const home(getenv("HOME"));
580 63 : if(home != nullptr
581 59 : && *home != '\0')
582 : {
583 57 : return home + filename.substr(1);
584 : }
585 : }
586 :
587 517 : return filename;
588 : }
589 :
590 :
591 : /** \brief Check whether a value represents "true".
592 : *
593 : * This function checks a string to see whether it is one of:
594 : *
595 : * * "true"
596 : * * "on"
597 : * * "yes"
598 : * * "1"
599 : *
600 : * If so, then the function returns true.
601 : *
602 : * \param[in] s The string to be checked.
603 : *
604 : * \return true if the string represents "true".
605 : */
606 9 : bool is_true(std::string s)
607 : {
608 9 : return s == "true" || s == "on" || s == "yes" | s == "1";
609 : }
610 :
611 :
612 : /** \brief Check whether a value represents "false".
613 : *
614 : * This function checks a string to see whether it is one of:
615 : *
616 : * * "false"
617 : * * "off"
618 : * * "no"
619 : * * "0"
620 : *
621 : * If so, then the function returns true.
622 : *
623 : * \param[in] s The string to be checked.
624 : *
625 : * \return true if the string represents "false".
626 : */
627 11 : bool is_false(std::string s)
628 : {
629 11 : return s == "false" || s == "off" || s == "no" || s == "0";
630 : }
631 :
632 :
633 : /** \brief Retrieve the width of one line in your console.
634 : *
635 : * This function retrieves the width of the console in number of characters.
636 : *
637 : * If the process is not connected to a TTY, then the function returns 80.
638 : *
639 : * If the width is less than 40, the function returns 40.
640 : *
641 : * \return The width of the console screen.
642 : */
643 291 : size_t get_screen_width()
644 : {
645 291 : std::int64_t cols(80);
646 :
647 291 : if(isatty(STDOUT_FILENO))
648 : {
649 : // when running coverage, the output is redirected for logging purposes
650 : // which means that isatty() returns false -- so at this time I just
651 : // exclude those since they are unreachable from my standard Unit Tests
652 : //
653 : winsize w; // LCOV_EXCL_LINE
654 : if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) != -1) // LCOV_EXCL_LINE
655 : {
656 : cols = std::max(static_cast<unsigned short>(40), w.ws_col); // LCOV_EXCL_LINE
657 : }
658 : }
659 :
660 291 : return cols;
661 : }
662 :
663 :
664 : /** \brief Breakup a string on multiple lines.
665 : *
666 : * This function breaks up the specified \p line of text in one or more
667 : * strings to fit your output.
668 : *
669 : * The \p line_width represents the maximum number of characters that get
670 : * printed in a row.
671 : *
672 : * The \p option_width parameter is the number of characters in the left
673 : * margin. When dealing with a very long argument, this width is 3 characters.
674 : * When dealing with the help itself, it is expected to be around 30.
675 : *
676 : * \note
677 : * This function always makes sure that the resulting string ends with
678 : * a newline character unless the input \p line string is empty.
679 : *
680 : * \param[in] line The line to breakup.
681 : * \param[in] option_width The number of characters in the left margin.
682 : * \param[in] line_width The total number of characters in the output.
683 : *
684 : * \return The broken up line as required.
685 : */
686 562 : std::string breakup_line(
687 : std::string line
688 : , size_t const option_width
689 : , size_t const line_width)
690 : {
691 1124 : std::stringstream ss;
692 :
693 562 : size_t const width(line_width - option_width);
694 :
695 : // TODO: once we have C++17, avoid substr() using std::string_view instead
696 : //
697 : for(;;)
698 : {
699 2022 : std::string l;
700 1292 : std::string::size_type const nl(line.find('\n'));
701 1292 : if(nl != std::string::npos
702 456 : && nl < width)
703 : {
704 288 : l = line.substr(0, nl);
705 288 : line = line.substr(nl + 1);
706 : }
707 1004 : else if(line.size() <= width)
708 : {
709 562 : break;
710 : }
711 442 : else if(std::isspace(line[width]))
712 : {
713 : // special case when the space is right at the edge
714 : //
715 38 : l = line.substr(0, width);
716 38 : size_t pos(width);
717 2 : do
718 : {
719 40 : ++pos;
720 : }
721 40 : while(std::isspace(line[pos]));
722 38 : line = line.substr(pos);
723 : }
724 : else
725 : {
726 : // search for the last space before the edge of the screen
727 : //
728 404 : std::string::size_type pos(line.find_last_of(' ', width));
729 404 : if(pos == std::string::npos)
730 : {
731 : // no space found, cut right at the edge...
732 : // (this should be really rare)
733 : //
734 78 : l = line.substr(0, width);
735 78 : line = line.substr(width);
736 : }
737 : else
738 : {
739 : // we found a space, write everything up to that space
740 : //
741 326 : l = line.substr(0, pos);
742 :
743 : // remove additional spaces from the start of the next line
744 : do // LCOV_EXCL_LINE
745 : {
746 326 : ++pos;
747 : }
748 326 : while(std::isspace(line[pos]));
749 326 : line = line.substr(pos);
750 : }
751 : }
752 :
753 730 : ss << l
754 730 : << std::endl;
755 :
756 : // more to print? if so we need the indentation
757 : //
758 1460 : if(!line.empty()
759 730 : && option_width > 0)
760 : {
761 202 : ss << std::setw(option_width) << " ";
762 : }
763 730 : }
764 :
765 : // some leftover?
766 : //
767 562 : if(!line.empty())
768 : {
769 550 : ss << line << std::endl;
770 : }
771 :
772 1124 : return ss.str();
773 : }
774 :
775 :
776 : /** \brief Format a help string to make it fit on a given width.
777 : *
778 : * This function properly wraps a set of help strings so they fit in
779 : * your console. The width has to be given by you at the moment.
780 : *
781 : * The function takes two strings, the argument with it's options
782 : * and the actual help string for that argument. If the argument
783 : * is short enough, it will appear on the first line with the
784 : * first line of help. If not, then one whole line is reserved
785 : * just for the argument and the help starts on the next line.
786 : *
787 : * \param[in] argument The option name with -- and arguments.
788 : * \param[in] help The help string for this argument.
789 : * \param[in] option_width Number of characters reserved for the option.
790 : * \param[in] line_width The maximum number of characters to display in width.
791 : *
792 : * \return A help string formatted for display.
793 : */
794 326 : std::string format_usage_string(
795 : std::string const & argument
796 : , std::string const & help
797 : , size_t const option_width
798 : , size_t const line_width)
799 : {
800 652 : std::stringstream ss;
801 :
802 326 : ss << " ";
803 :
804 326 : if(argument.size() < option_width - 3)
805 : {
806 : // enough space on a single line
807 : //
808 : ss << argument
809 266 : << std::setw(option_width - 3 - argument.size())
810 532 : << " ";
811 : }
812 60 : else if(argument.size() >= line_width - 4)
813 : {
814 : // argument too long for even one line on the screen!?
815 : // call the function to break it up with indentation of 3
816 : //
817 2 : ss << breakup_line(argument, 3, line_width);
818 :
819 4 : if(!help.empty()
820 2 : && option_width > 0)
821 : {
822 2 : ss << std::setw(option_width) << " ";
823 : }
824 : }
825 : else
826 : {
827 : // argument too long for the help to follow immediately
828 : //
829 58 : ss << argument
830 58 : << std::endl
831 : << std::setw(option_width)
832 58 : << " ";
833 : }
834 :
835 326 : ss << breakup_line(help, option_width, line_width);
836 :
837 652 : return ss.str();
838 : }
839 :
840 :
841 : /** \brief Escape special characters from a shell argument.
842 : *
843 : * This function goes through the supplied argument. If it includes one
844 : * or more character other than `[-+0-9A-Za-z_]`, then it gets \em escaped.
845 : * This means we add single quotes at the start and end, and escape any
846 : * single quote within the argument.
847 : *
848 : * So the function may return the input string as is.
849 : *
850 : * \param[in] arg The argument to escape.
851 : *
852 : * \return The escaped argument.
853 : */
854 110 : std::string escape_shell_argument(std::string const & arg)
855 : {
856 110 : if(arg.empty())
857 : {
858 1 : return std::string(g_empty_string);
859 : }
860 :
861 109 : std::string::size_type const pos(arg.find_first_not_of(g_simple_characters));
862 109 : if(pos == std::string::npos)
863 : {
864 106 : return arg;
865 : }
866 :
867 6 : std::string result;
868 :
869 3 : result += g_single_quote;
870 3 : std::string::size_type p1(0);
871 9 : while(p1 < arg.length())
872 : {
873 5 : std::string::size_type const p2(arg.find('\'', p1));
874 5 : if(p2 == std::string::npos)
875 : {
876 2 : result += arg.substr(p1);
877 2 : break;
878 : }
879 3 : result += arg.substr(p1, p2 - p1);
880 3 : result += g_escaped_single_quotes;
881 3 : p1 = p2 + 1; // skip the '
882 : }
883 3 : result += g_single_quote;
884 :
885 3 : return result;
886 : }
887 :
888 :
889 : /** \brief Generate a string describing whether we're using the sanitizer.
890 : *
891 : * This function determines whether this library was compiled with the
892 : * sanitizer extension. If so, then it will return detail about which
893 : * feature was compiled in.
894 : *
895 : * If no sanitizer options were compiled in, then it returns a
896 : * message saying so.
897 : *
898 : * \return A string with details about the sanitizer.
899 : */
900 2 : std::string sanitizer_details()
901 : {
902 2 : std::string result;
903 : #if defined(__SANITIZE_ADDRESS__) || defined(__SANITIZE_THREAD__)
904 : #if defined(__SANITIZE_ADDRESS__)
905 2 : result += "The address sanitizer is compiled in.\n";
906 : #endif
907 : #if defined(__SANITIZE_THREAD__)
908 : result += "The thread sanitizer is compiled in.\n";
909 : #endif
910 : #else
911 : result += "The address and thread sanitizers are not compiled in.\n";
912 : #endif
913 2 : return result;
914 : }
915 :
916 :
917 : /** \brief Retrieve the height of your console.
918 : *
919 : * This function retrieves the height of the console in number of characters.
920 : * This is also called the number of rows.
921 : *
922 : * If the process is not connected to a TTY, then the function returns 25.
923 : *
924 : * If the height is less than 2, the function returns 2.
925 : *
926 : * \note
927 : * The get_screen_width() and get_screen_height() should be combined.
928 : *
929 : * \return The width of the console screen.
930 : */
931 0 : size_t get_screen_height()
932 : {
933 0 : std::int64_t rows(25);
934 :
935 0 : if(isatty(STDOUT_FILENO))
936 : {
937 : // when running coverage, the output is redirected for logging purposes
938 : // which means that isatty() returns false -- so at this time I just
939 : // exclude those since they are unreachable from my standard Unit Tests
940 : //
941 : winsize w; // LCOV_EXCL_LINE
942 : if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) != -1) // LCOV_EXCL_LINE
943 : {
944 : rows = std::max(static_cast<unsigned short>(2), w.ws_row); // LCOV_EXCL_LINE
945 : }
946 : }
947 :
948 0 : return rows;
949 : }
950 :
951 :
952 : /** \brief Print out a string to the console or use less.
953 : *
954 : * If the \p data string to be output is too large for the screen (too
955 : * many lines; we assume the width was already "fixed") then use less
956 : * to show the data. If less is not available, use more. If neither
957 : * is available, fall back to printing everything at once.
958 : *
959 : * \param[in,out] out The output stream where the data has to be written.
960 : * \param[in] data The data to be written to stream.
961 : */
962 9 : void less(std::basic_ostream<char> & out, std::string const & data)
963 : {
964 9 : if(snapdev::isatty(out))
965 : {
966 0 : auto const lines(std::count(data.begin(), data.end(), '\n'));
967 0 : size_t const height(get_screen_height());
968 0 : if(lines > static_cast<std::remove_const_t<decltype(lines)>>(height))
969 : {
970 0 : struct stat s;
971 0 : if(stat("/bin/less", &s) == 0)
972 : {
973 0 : FILE * f(popen("/bin/less", "w"));
974 0 : if(f != nullptr)
975 : {
976 0 : fwrite(data.c_str(), sizeof(char), data.length(), f);
977 0 : pclose(f);
978 0 : return;
979 : }
980 : }
981 0 : else if(stat("/bin/more", &s) == 0)
982 : {
983 0 : FILE * f(popen("/bin/more", "w"));
984 0 : if(f != nullptr)
985 : {
986 0 : fwrite(data.c_str(), sizeof(char), data.length(), f);
987 0 : pclose(f);
988 0 : return;
989 : }
990 : }
991 : }
992 : }
993 :
994 : // fallback, just print everything to the console as is
995 : //
996 9 : out << data << std::endl;
997 : }
998 :
999 :
1000 :
1001 6 : } // namespace advgetopt
1002 : // vim: ts=4 sw=4 et
|