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