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