Line data Source code
1 : /*
2 : * License:
3 : * Copyright (c) 2006-2021 Made to Order Software Corp. All Rights Reserved
4 : *
5 : * https://snapwebsites.org/project/advgetopt
6 : * contact@m2osw.com
7 : *
8 : * This program is free software; you can redistribute it and/or modify
9 : * it under the terms of the GNU General Public License as published by
10 : * the Free Software Foundation; either version 2 of the License, or
11 : * (at your option) any later version.
12 : *
13 : * This program is distributed in the hope that it will be useful,
14 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 : * GNU General Public License for more details.
17 : *
18 : * You should have received a copy of the GNU General Public License along
19 : * with this program; if not, write to the Free Software Foundation, Inc.,
20 : * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 : *
22 : * Authors:
23 : * Alexis Wilke alexis@m2osw.com
24 : * Doug Barbieri doug@m2osw.com
25 : */
26 :
27 : /** \file
28 : * \brief Advanced getopt usage() implementation.
29 : *
30 : * The advgetopt class usage() and helper functions are grouped in this
31 : * file.
32 : */
33 :
34 : // self
35 : //
36 : #include "advgetopt/advgetopt.h"
37 :
38 :
39 : // advgetopt lib
40 : //
41 : #include "advgetopt/exception.h"
42 :
43 :
44 : // C++ lib
45 : //
46 : #include <iomanip>
47 : #include <iostream>
48 :
49 :
50 : // C lib
51 : //
52 : #include <unistd.h>
53 : #include <sys/ioctl.h>
54 :
55 :
56 : // last include
57 : //
58 : #include <snapdev/poison.h>
59 :
60 :
61 :
62 :
63 : namespace advgetopt
64 : {
65 :
66 :
67 :
68 : /** \brief Transform group names in --\<name>-help commands.
69 : *
70 : * This function allows for the group names to be transformed into help
71 : * command line options.
72 : */
73 331 : void getopt::parse_options_from_group_names()
74 : {
75 : // add the --long-help if at least one option uses the GROUP1 or GROUP2
76 : //
77 707 : for(auto it(f_options_by_name.begin())
78 707 : ; it != f_options_by_name.end()
79 : ; ++it)
80 : {
81 384 : if(it->second->has_flag(GETOPT_FLAG_SHOW_GROUP1 | GETOPT_FLAG_SHOW_GROUP2))
82 : {
83 16 : option_info::pointer_t opt(std::make_shared<option_info>("long-help"));
84 8 : opt->add_flag(GETOPT_FLAG_COMMAND_LINE
85 : | GETOPT_FLAG_FLAG
86 : | GETOPT_FLAG_GROUP_COMMANDS);
87 8 : opt->set_help("show all the help from all the available options.");
88 8 : f_options_by_name["long-help"] = opt;
89 8 : if(f_options_by_short_name.find(L'?') == f_options_by_short_name.end())
90 : {
91 8 : opt->set_short_name(L'?');
92 8 : f_options_by_short_name[L'?'] = opt;
93 : }
94 8 : break;
95 : }
96 : }
97 :
98 331 : if(f_options_environment.f_groups == nullptr)
99 : {
100 : // no groups, ignore following loop
101 : //
102 297 : return;
103 : }
104 :
105 102 : for(group_description const * grp = f_options_environment.f_groups
106 102 : ; grp->f_group != GETOPT_FLAG_GROUP_NONE
107 : ; ++grp)
108 : {
109 : // the name is not mandatory, without it you do not get the command
110 : // line option but still get the group description
111 : //
112 68 : if(grp->f_name != nullptr
113 6 : && *grp->f_name != '\0')
114 : {
115 12 : std::string const name(grp->f_name);
116 12 : std::string const option_name(name + "-help");
117 12 : option_info::pointer_t opt(std::make_shared<option_info>(option_name));
118 6 : opt->add_flag(GETOPT_FLAG_COMMAND_LINE
119 : | GETOPT_FLAG_FLAG
120 : | GETOPT_FLAG_GROUP_COMMANDS);
121 12 : opt->set_help("show help from the \""
122 12 : + name
123 18 : + "\" group of options.");
124 6 : f_options_by_name[option_name] = opt;
125 : }
126 : }
127 : }
128 :
129 :
130 : /** \brief Search for \p group in the list of group names.
131 : *
132 : * This function is used to search for the name of a group.
133 : *
134 : * Groups are used by the usage() function to list options by some user
135 : * selected group.
136 : *
137 : * For example, it is often that a tool has a set of commands such as
138 : * `--delete` and a set of options such as `--verbose`. These can represent
139 : * to clear groups of commands and options.
140 : *
141 : * \param[in] group The group to look for (i.e. GETOPT_FLAG_GROUP_ONE).
142 : *
143 : * \return The group structure or nullptr when not found.
144 : */
145 45 : group_description const * getopt::find_group(flag_t group) const
146 : {
147 45 : if(f_options_environment.f_groups == nullptr)
148 : {
149 2 : return nullptr;
150 : }
151 :
152 43 : if((group & ~GETOPT_FLAG_GROUP_MASK) != 0)
153 : {
154 29 : throw getopt_logic_error("group parameter must represent a valid group.");
155 : }
156 14 : if(group == GETOPT_FLAG_GROUP_NONE)
157 : {
158 1 : throw getopt_logic_error("group NONE cannot be assigned a name so you cannot search for it.");
159 : }
160 :
161 21 : for(group_description const * grp(f_options_environment.f_groups)
162 21 : ; grp->f_group != GETOPT_FLAG_GROUP_NONE
163 : ; ++grp)
164 : {
165 20 : if(group == grp->f_group)
166 : {
167 12 : if((grp->f_name == nullptr || *grp->f_name == '\0')
168 2 : && (grp->f_description == nullptr || *grp->f_description == '\0'))
169 : {
170 2 : throw getopt_logic_error("at least one of a group name or description must be defined (a non-empty string).");
171 : }
172 10 : return grp;
173 : }
174 : }
175 :
176 : // group not defined
177 : //
178 1 : return nullptr;
179 : }
180 :
181 :
182 : /** \brief Create a string of the command line arguments.
183 : *
184 : * This function assembles the command line arguments in a string and
185 : * returns that string.
186 : *
187 : * The function has the ability to wrap strings around for better formatting.
188 : *
189 : * The list of arguments to show is defined by the \p show parameter. When
190 : * \p show is 0, then only the regular and error arguments are shown.
191 : * Otherwise only the argumenst with the specified flags are show. Only
192 : * the `..._SHOW_...` flags are valid here.
193 : *
194 : * When an error occurs, it is customary to set \p show to
195 : * GETOPT_FLAG_SHOW_USAGE_ON_ERROR so only a limited set of arguments
196 : * are shown.
197 : *
198 : * The library offers two groups in case you have a command line tools
199 : * with a large number of options, those two can be used to only show
200 : * those specific set of options with using a specific `--help` argument.
201 : *
202 : * \note
203 : * This function does NOT print anything in the output. This is your
204 : * responsibility. We do it this way because you may be using a logger
205 : * and not want to print the usage in the \em wrong destination.
206 : *
207 : * \bug
208 : * The options are written from our map. This means the order will be
209 : * alphabetical and not the order in which you defined the options.
210 : * We are not looking into fixing this problem. That's just something
211 : * you want to keep in mind.
212 : *
213 : * \param[in] show Selection of the options to show.
214 : *
215 : * \return The assembled command line arguments.
216 : */
217 79 : std::string getopt::usage( flag_t show ) const
218 : {
219 158 : std::stringstream ss;
220 :
221 79 : flag_t specific_group(show & GETOPT_FLAG_GROUP_MASK);
222 :
223 : // ignore all the non-show flags
224 : //
225 79 : show &= GETOPT_FLAG_SHOW_USAGE_ON_ERROR
226 : | GETOPT_FLAG_SHOW_ALL
227 : | GETOPT_FLAG_SHOW_GROUP1
228 : | GETOPT_FLAG_SHOW_GROUP2;
229 :
230 79 : size_t const line_width(get_line_width());
231 79 : ss << breakup_line(process_help_string(f_options_environment.f_help_header), 0, line_width);
232 :
233 158 : std::string save_default;
234 158 : std::string save_help;
235 :
236 79 : flag_t pos(GETOPT_FLAG_GROUP_MINIMUM);
237 79 : flag_t group_max(GETOPT_FLAG_GROUP_MAXIMUM);
238 79 : if(f_options_environment.f_groups == nullptr)
239 : {
240 72 : group_max = GETOPT_FLAG_GROUP_MINIMUM;
241 72 : specific_group = GETOPT_FLAG_GROUP_NONE;
242 : }
243 7 : else if(specific_group != GETOPT_FLAG_GROUP_NONE)
244 : {
245 : // only display that specific group if asked to do so
246 : //
247 2 : pos = specific_group >> GETOPT_FLAG_GROUP_SHIFT;
248 2 : group_max = pos;
249 : }
250 :
251 307 : for(; pos <= group_max; ++pos)
252 : {
253 114 : bool group_name_shown(false);
254 114 : flag_t const group(pos << GETOPT_FLAG_GROUP_SHIFT);
255 885 : for(auto const & opt : f_options_by_name)
256 : {
257 1542 : if((opt.second->get_flags() & GETOPT_FLAG_GROUP_MASK) != group
258 771 : && f_options_environment.f_groups != nullptr)
259 : {
260 : // this could be optimized but we'd probably not see much
261 : // difference overall and it's just for the usage() call
262 : //
263 506 : continue;
264 : }
265 :
266 444 : std::string const help(opt.second->get_help());
267 265 : if(help.empty())
268 : {
269 : // ignore entries without help
270 : //
271 10 : continue;
272 : }
273 :
274 255 : if(opt.second->has_flag(GETOPT_FLAG_ALIAS))
275 : {
276 : // ignore entries representing an alias
277 : //
278 12 : continue;
279 : }
280 :
281 243 : if((show & GETOPT_FLAG_SHOW_ALL) == 0)
282 : {
283 183 : if(show != 0)
284 : {
285 66 : if(!opt.second->has_flag(show))
286 : {
287 : // usage selected group is not present in this option, ignore
288 : //
289 58 : continue;
290 : }
291 : }
292 117 : else if(opt.second->has_flag(GETOPT_FLAG_SHOW_GROUP1 | GETOPT_FLAG_SHOW_GROUP2))
293 : {
294 : // do not show specialized groups
295 : //
296 6 : continue;
297 : }
298 : }
299 :
300 179 : if(!group_name_shown)
301 : {
302 84 : group_name_shown = true;
303 :
304 84 : if(group != GETOPT_FLAG_GROUP_NONE)
305 : {
306 8 : group_description const * grp(find_group(group));
307 8 : if(grp != nullptr)
308 : {
309 8 : ss << std::endl
310 8 : << breakup_line(process_help_string(grp->f_description), 0, line_width);
311 : }
312 : }
313 : }
314 :
315 358 : std::stringstream argument;
316 :
317 179 : if(opt.second->is_default_option())
318 : {
319 6 : switch(opt.second->get_flags() & (GETOPT_FLAG_REQUIRED | GETOPT_FLAG_MULTIPLE))
320 : {
321 1 : case 0:
322 1 : argument << "[default argument]";
323 1 : break;
324 :
325 1 : case GETOPT_FLAG_REQUIRED:
326 1 : argument << "<default argument>";
327 1 : break;
328 :
329 2 : case GETOPT_FLAG_MULTIPLE:
330 2 : argument << "[default arguments]";
331 2 : break;
332 :
333 2 : case GETOPT_FLAG_REQUIRED | GETOPT_FLAG_MULTIPLE:
334 2 : argument << "<default arguments>";
335 2 : break;
336 :
337 : }
338 : }
339 : else
340 : {
341 173 : argument << "--" << opt.second->get_name();
342 173 : if(opt.second->get_short_name() != NO_SHORT_NAME)
343 : {
344 49 : argument << " or -" << short_name_to_string(opt.second->get_short_name());
345 : }
346 :
347 173 : switch(opt.second->get_flags() & (GETOPT_FLAG_FLAG | GETOPT_FLAG_REQUIRED | GETOPT_FLAG_MULTIPLE))
348 : {
349 1 : case 0:
350 1 : argument << " [<arg>]";
351 1 : break;
352 :
353 33 : case GETOPT_FLAG_REQUIRED:
354 33 : argument << " <arg>";
355 33 : break;
356 :
357 8 : case GETOPT_FLAG_MULTIPLE:
358 8 : argument << " {<arg>}";
359 8 : break;
360 :
361 6 : case GETOPT_FLAG_REQUIRED | GETOPT_FLAG_MULTIPLE:
362 6 : argument << " <arg> {<arg>}";
363 6 : break;
364 :
365 : }
366 : }
367 :
368 179 : if(opt.second->has_default())
369 : {
370 : argument << " (default is \""
371 9 : << opt.second->get_default()
372 18 : << "\")";
373 : }
374 :
375 : // Output argument string with help
376 : //
377 179 : if(opt.second->is_default_option())
378 : {
379 6 : save_default = argument.str();
380 6 : save_help = help;
381 : }
382 : else
383 : {
384 346 : ss << format_usage_string(argument.str()
385 346 : , process_help_string(help.c_str())
386 : , 30
387 : , line_width);
388 : }
389 : }
390 : }
391 :
392 79 : if(!save_default.empty())
393 : {
394 12 : ss << format_usage_string(save_default
395 12 : , process_help_string(save_help.c_str())
396 : , 30
397 : , line_width);
398 : }
399 :
400 79 : if(f_options_environment.f_help_footer != nullptr
401 77 : && f_options_environment.f_help_footer[0] != '\0')
402 : {
403 77 : ss << std::endl;
404 77 : ss << breakup_line(process_help_string(f_options_environment.f_help_footer), 0, line_width);
405 : }
406 :
407 158 : return ss.str();
408 : }
409 :
410 :
411 : /** \brief Change the % flags in help strings.
412 : *
413 : * This function goes through the help string and replaces the `%\<flag>`
414 : * with various content available in the getopt object.
415 : *
416 : * This is helpful for various reasons. For example, you may use the
417 : * same set of options in several different programs, in which case the
418 : * `%p` is likely useful to print out the name of the program currently
419 : * in use.
420 : *
421 : * Similarly we offer ways to print out lists of configuration files,
422 : * the environment variable name & value, etc. The following is the
423 : * list of supported flags:
424 : *
425 : * \li "%%" -- print out a percent
426 : * \li "%a" -- print out the project name (a.k.a. application name)
427 : * \li "%b" -- print out the build date
428 : * \li "%c" -- print out the copyright notice
429 : * \li "%d" -- print out the first directory with configuration files.
430 : * \li "%*d" -- print out the complete list of directories with configuration
431 : * files.
432 : * \li "%e" -- print out the name of the environment variable.
433 : * \li "%*e" -- print out the name and value of the environment variable.
434 : * \li "%f" -- print out the first configuration path and filename.
435 : * \li "%*f" -- print out all the configuration full paths.
436 : * \li "%g" -- print out the list of existing configuration files.
437 : * \li "%*g" -- print out the list of all possible configuration files.
438 : * \li "%i" -- print out the directory to option files.
439 : * \li "%l" -- print out the license.
440 : * \li "%o" -- show the configuration filename where changes get written.
441 : * \li "%p" -- print out the program basename.
442 : * \li "%*p" -- print out the full program name.
443 : * \li "%s" -- print out the group name.
444 : * \li "%t" -- print out the build time.
445 : * \li "%v" -- print out the version.
446 : * \li "%w" -- print out the list of all the writable configuration files.
447 : *
448 : * Here is an example where the `%p` can be used:
449 : *
450 : * \code
451 : * "Usage: %p [-opt] filename ..."
452 : * \endcode
453 : *
454 : * The other flags are more often used in places like the copyright notice
455 : * the footer, the license notice, etc.
456 : *
457 : * \param[in] help A string that may include `%` flags.
458 : *
459 : * \return The string with any '%\<flag>' replaced.
460 : *
461 : * \sa parse_program_name()
462 : */
463 344 : std::string getopt::process_help_string(char const * help) const
464 : {
465 344 : if(help == nullptr)
466 : {
467 1 : return std::string();
468 : }
469 :
470 686 : std::string result;
471 :
472 38937 : while(help[0] != '\0')
473 : {
474 19297 : if(help[0] == '%')
475 : {
476 410 : switch(help[1])
477 : {
478 13 : case '%':
479 13 : result += '%';
480 13 : help += 2;
481 13 : break;
482 :
483 98 : case '*':
484 98 : switch(help[2])
485 : {
486 19 : case 'd':
487 19 : if(f_options_environment.f_configuration_directories != nullptr)
488 : {
489 16 : bool first(true);
490 68 : for(char const * const * directories(f_options_environment.f_configuration_directories)
491 68 : ; *directories != nullptr
492 : ; ++directories)
493 : {
494 52 : if(first)
495 : {
496 13 : first = false;
497 : }
498 : else
499 : {
500 39 : result += ", ";
501 : }
502 52 : result += *directories;
503 : }
504 : }
505 19 : help += 3;
506 19 : break;
507 :
508 28 : case 'e':
509 28 : if(f_options_environment.f_environment_variable_name != nullptr
510 22 : && *f_options_environment.f_environment_variable_name != '\0')
511 : {
512 16 : result += f_options_environment.f_environment_variable_name;
513 16 : char const * env(getenv(f_options_environment.f_environment_variable_name));
514 16 : if(env != nullptr)
515 : {
516 3 : result += '=';
517 3 : result += env;
518 : }
519 : else
520 : {
521 13 : result += " (not set)";
522 : }
523 : }
524 28 : help += 3;
525 28 : break;
526 :
527 19 : case 'f':
528 19 : if(f_options_environment.f_configuration_files != nullptr)
529 : {
530 16 : bool first(true);
531 68 : for(char const * const * filenames(f_options_environment.f_configuration_files)
532 68 : ; *filenames != nullptr
533 : ; ++filenames)
534 : {
535 52 : if(first)
536 : {
537 13 : first = false;
538 : }
539 : else
540 : {
541 39 : result += ", ";
542 : }
543 52 : result += *filenames;
544 : }
545 : }
546 19 : help += 3;
547 19 : break;
548 :
549 19 : case 'g':
550 : {
551 38 : string_list_t list(get_configuration_filenames(false, false));
552 19 : bool first(true);
553 193 : for(auto n : list)
554 : {
555 174 : if(first)
556 : {
557 13 : first = false;
558 : }
559 : else
560 : {
561 161 : result += ", ";
562 : }
563 174 : result += n;
564 : }
565 38 : help += 3;
566 : }
567 19 : break;
568 :
569 13 : case 'p':
570 13 : result += f_program_fullname;
571 13 : help += 3;
572 13 : break;
573 :
574 : }
575 98 : break;
576 :
577 19 : case 'a':
578 19 : if(f_options_environment.f_project_name != nullptr)
579 : {
580 16 : result += f_options_environment.f_project_name;
581 : }
582 19 : help += 2;
583 19 : break;
584 :
585 19 : case 'b':
586 19 : if(f_options_environment.f_build_date != nullptr)
587 : {
588 16 : result += f_options_environment.f_build_date;
589 : }
590 19 : help += 2;
591 19 : break;
592 :
593 19 : case 'c':
594 19 : if(f_options_environment.f_copyright != nullptr)
595 : {
596 16 : result += f_options_environment.f_copyright;
597 : }
598 19 : help += 2;
599 19 : break;
600 :
601 19 : case 'd':
602 19 : if(f_options_environment.f_configuration_directories != nullptr
603 16 : && *f_options_environment.f_configuration_directories != nullptr)
604 : {
605 13 : result += *f_options_environment.f_configuration_directories;
606 : }
607 19 : help += 2;
608 19 : break;
609 :
610 28 : case 'e':
611 28 : if(f_options_environment.f_environment_variable_name != nullptr)
612 : {
613 22 : result += f_options_environment.f_environment_variable_name;
614 : }
615 28 : help += 2;
616 28 : break;
617 :
618 19 : case 'f':
619 19 : if(f_options_environment.f_configuration_files != nullptr
620 16 : && *f_options_environment.f_configuration_files != nullptr)
621 : {
622 13 : result += *f_options_environment.f_configuration_files;
623 : }
624 19 : help += 2;
625 19 : break;
626 :
627 22 : case 'g':
628 : {
629 44 : string_list_t list(get_configuration_filenames(true, false));
630 22 : bool first(true);
631 34 : for(auto n : list)
632 : {
633 12 : if(first)
634 : {
635 6 : first = false;
636 : }
637 : else
638 : {
639 6 : result += ", ";
640 : }
641 12 : result += n;
642 : }
643 44 : help += 2;
644 : }
645 22 : break;
646 :
647 19 : case 'i':
648 : // in the advgetopt_options.cpp, we clearly add a final "/"
649 : // so we want to add it here too, to be consistent
650 : {
651 38 : std::string directory("/usr/share/advgetopt/options/");
652 19 : if(f_options_environment.f_options_files_directory != nullptr
653 16 : && *f_options_environment.f_options_files_directory != '\0')
654 : {
655 13 : directory = f_options_environment.f_options_files_directory;
656 13 : if(directory.back() != '/')
657 : {
658 13 : directory += '/';
659 : }
660 : }
661 38 : result += directory;
662 : }
663 19 : help += 2;
664 19 : break;
665 :
666 19 : case 'l':
667 19 : if(f_options_environment.f_license != nullptr)
668 : {
669 16 : result += f_options_environment.f_license;
670 : }
671 19 : help += 2;
672 19 : break;
673 :
674 19 : case 'o':
675 : {
676 38 : string_list_t const list(get_configuration_filenames(false, true));
677 19 : if(!list.empty())
678 : {
679 13 : result += list.back();
680 : }
681 38 : help += 2;
682 : }
683 19 : break;
684 :
685 32 : case 'p':
686 32 : result += f_program_name;
687 32 : help += 2;
688 32 : break;
689 :
690 5 : case 's':
691 5 : if(f_options_environment.f_group_name != nullptr)
692 : {
693 5 : result += f_options_environment.f_group_name;
694 : }
695 5 : help += 2;
696 5 : break;
697 :
698 19 : case 't':
699 19 : if(f_options_environment.f_build_time != nullptr)
700 : {
701 16 : result += f_options_environment.f_build_time;
702 : }
703 19 : help += 2;
704 19 : break;
705 :
706 19 : case 'v':
707 19 : if(f_options_environment.f_version != nullptr)
708 : {
709 16 : result += f_options_environment.f_version;
710 : }
711 19 : help += 2;
712 19 : break;
713 :
714 22 : case 'w':
715 : {
716 44 : string_list_t const list(get_configuration_filenames(true, true));
717 22 : bool first(true);
718 31 : for(auto n : list)
719 : {
720 9 : if(first)
721 : {
722 6 : first = false;
723 : }
724 : else
725 : {
726 3 : result += ", ";
727 : }
728 9 : result += n;
729 : }
730 44 : help += 2;
731 : }
732 22 : break;
733 :
734 : }
735 : }
736 : else
737 : {
738 18887 : result += help[0];
739 18887 : ++help;
740 : }
741 : }
742 :
743 343 : return result;
744 : }
745 :
746 :
747 : /** \brief Format a help string to make it fit on a given width.
748 : *
749 : * This function properly wraps a set of help strings so they fit in
750 : * your console. The width has to be given by you at the moment.
751 : *
752 : * The function takes two strings, the argument with it's options
753 : * and the actual help string for that argument. If the argument
754 : * is short enough, it will appear on the first line with the
755 : * first line of help. If not, then one whole line is reserved
756 : * just for the argument and the help starts on the next line.
757 : *
758 : * \param[in] argument The option name with -- and arguments.
759 : * \param[in] help The help string for this argument.
760 : * \param[in] option_width Number of characters reserved for the option.
761 : * \param[in] line_width The maximum number of characters to display in width.
762 : *
763 : * \return A help string formatted for display.
764 : */
765 318 : std::string getopt::format_usage_string(
766 : std::string const & argument
767 : , std::string const & help
768 : , size_t const option_width
769 : , size_t const line_width)
770 : {
771 636 : std::stringstream ss;
772 :
773 318 : ss << " ";
774 :
775 318 : if(argument.size() < option_width - 3)
776 : {
777 : // enough space on a single line
778 : //
779 : ss << argument
780 258 : << std::setw(option_width - 3 - argument.size())
781 516 : << " ";
782 : }
783 60 : else if(argument.size() >= line_width - 4)
784 : {
785 : // argument too long for even one line on the screen!?
786 : // call the function to break it up with indentation of 3
787 : //
788 2 : ss << breakup_line(argument, 3, line_width);
789 :
790 4 : if(!help.empty()
791 2 : && option_width > 0)
792 : {
793 2 : ss << std::setw(option_width) << " ";
794 : }
795 : }
796 : else
797 : {
798 : // argument too long for the help to follow immediately
799 : //
800 58 : ss << argument
801 58 : << std::endl
802 : << std::setw(option_width)
803 58 : << " ";
804 : }
805 :
806 318 : ss << breakup_line(help, option_width, line_width);
807 :
808 636 : return ss.str();
809 : }
810 :
811 :
812 : /** \brief Breakup a string on multiple lines.
813 : *
814 : * This function breaks up the specified \p line of text in one or more
815 : * strings to fit your output.
816 : *
817 : * The \p line_width represents the maximum number of characters that get
818 : * printed in a row.
819 : *
820 : * The \p option_width parameter is the number of characters in the left
821 : * margin. When dealing with a very long argument, this width is 3 characters.
822 : * When dealing with the help itself, it is expected to be around 30.
823 : *
824 : * \note
825 : * This function always makes sure that the resulting string ends with
826 : * a newline character unless the input \p line string is empty.
827 : *
828 : * \param[in] line The line to breakup.
829 : * \param[in] option_width The number of characters in the left margin.
830 : * \param[in] line_width The total number of characters in the output.
831 : *
832 : * \return The broken up line as required.
833 : */
834 550 : std::string getopt::breakup_line(std::string line
835 : , size_t const option_width
836 : , size_t const line_width)
837 : {
838 1100 : std::stringstream ss;
839 :
840 550 : size_t const width(line_width - option_width);
841 :
842 : // TODO: once we have C++17, avoid substr() using std::string_view instead
843 : //
844 1222 : while(line.size() > width)
845 : {
846 1344 : std::string l;
847 672 : std::string::size_type const nl(line.find('\n'));
848 672 : if(nl != std::string::npos
849 404 : && nl < width)
850 : {
851 236 : l = line.substr(0, nl);
852 236 : line = line.substr(nl + 1);
853 : }
854 436 : else if(std::isspace(line[width]))
855 : {
856 : // special case when the space is right at the edge
857 : //
858 38 : l = line.substr(0, width);
859 38 : size_t pos(width);
860 2 : do
861 : {
862 40 : ++pos;
863 : }
864 40 : while(std::isspace(line[pos]));
865 38 : line = line.substr(pos);
866 : }
867 : else
868 : {
869 : // search for the last space before the edge of the screen
870 : //
871 398 : std::string::size_type pos(line.find_last_of(' ', width));
872 398 : if(pos == std::string::npos)
873 : {
874 : // no space found, cut right at the edge...
875 : // (this should be really rare)
876 : //
877 78 : l = line.substr(0, width);
878 78 : line = line.substr(width);
879 : }
880 : else
881 : {
882 : // we found a space, write everything up to that space
883 : //
884 320 : l = line.substr(0, pos);
885 :
886 : // remove additional spaces from the start of the next line
887 : do // LCOV_EXCL_LINE
888 : {
889 320 : ++pos;
890 : }
891 320 : while(std::isspace(line[pos]));
892 320 : line = line.substr(pos);
893 : }
894 : }
895 :
896 672 : ss << l
897 672 : << std::endl;
898 :
899 : // more to print? if so we need the indentation
900 : //
901 1344 : if(!line.empty()
902 672 : && option_width > 0)
903 : {
904 192 : ss << std::setw( option_width ) << " ";
905 : }
906 : }
907 :
908 : // some leftover?
909 : //
910 550 : if(!line.empty())
911 : {
912 550 : ss << line << std::endl;
913 : }
914 :
915 1100 : return ss.str();
916 : }
917 :
918 :
919 :
920 : /** \brief Retrieve the width of one line in your console.
921 : *
922 : * This function retrieves the width of the console in number of characters.
923 : *
924 : * If the process is not connected to a TTY, then the function returns 80.
925 : *
926 : * If the width is less than 40, the function returns 40.
927 : *
928 : * \return The width of the console screen.
929 : */
930 284 : size_t getopt::get_line_width()
931 : {
932 284 : std::int64_t cols(80);
933 :
934 284 : if(isatty(STDOUT_FILENO))
935 : {
936 : // when running coverage, the output is redirected for logging purposes
937 : // which means that isatty() returns false -- so at this time I just
938 : // exclude those since they are unreachable from my standard Unit Tests
939 : //
940 0 : winsize w;
941 : if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) != -1) // LCOV_EXCL_LINE
942 : {
943 : cols = std::max(static_cast<unsigned short>(40), w.ws_col); // LCOV_EXCL_LINE
944 : }
945 : }
946 :
947 284 : return cols;
948 : }
949 :
950 :
951 :
952 6 : } // namespace advgetopt
953 : // vim: ts=4 sw=4 et
|