/* * Output command-line help to stdout. * * Copyright 2002-2008, 2010, 2012-2015, 2017, 2021, 2023-2025 Andrew Wood * * License GPLv3+: GNU GPL version 3 or later; see `docs/COPYING'. */ #include "config.h" #include "pv.h" #include #include #include #include #include #if defined(ENABLE_NLS) && defined(HAVE_WCHAR_H) #include #if defined(HAVE_WCTYPE_H) #include #endif #endif /* * Return the number of display columns needed to show the given * null-terminated string. * * If NLS is disabled, or the string cannot be converted, this is just the * same as "strlen()". */ static size_t display_width(const char *string) { size_t bytes; if (NULL == string) return 0; bytes = strlen(string); /* flawfinder: ignore */ /* * flawfinder rationale: we have already checked for NULL, and it is * explicitly required of the caller to provide a null-terminated * string. */ return pv_strwidth(string, bytes); } /* * The 7-bit ASCII version of display_word_wrap() below - does not * understand multi-byte characters. */ static void display_word_wrap_7bit(const char *string, size_t display_width, size_t left_margin) { const char *start; const char *end; if (NULL == string) return; start = string; while (strlen(start) > display_width) { /* flawfinder: ignore */ /* flawfinder rationale: see above. */ end = start + display_width; while ((end > start) && (end[0] != ' ')) end--; if (end == start) { end = start + display_width; } else { end++; } printf("%.*s", (int) (end - start), start); if (end == start) end++; start = end; if (start[0] != '\0') printf("\n%*s", (int) left_margin, ""); } printf("%s", start); } /* * Output a null-terminated string to standard output, word wrapping to * "display_width" display character positions, and left-padding any new * lines after the first one with "left_margin" spaces. * * Wide characters are handled if NLS is enabled, but if they can't be, this * falls back to a version which just counts bytes as characters. */ static void display_word_wrap(const char *string, size_t display_width, size_t left_margin) { #if defined(ENABLE_NLS) && defined(HAVE_WCHAR_H) size_t wide_char_count; size_t wide_string_buffer_size; wchar_t *wide_string; size_t start_idx, end_idx, chars_remaining; if (NULL == string) return; /*@-nullpass@ */ /* splint note: see mbstowcs() call in pv_strwidth(). */ wide_char_count = mbstowcs(NULL, string, 0); /*@+nullpass@ */ if (wide_char_count == (size_t) -1) { debug("%s: %s: %s", "mbstowcs", string, strerror(errno)); display_word_wrap_7bit(string, display_width, left_margin); return; } wide_string_buffer_size = sizeof(*wide_string) * (1 + wide_char_count); wide_string = malloc(wide_string_buffer_size); if (NULL == wide_string) { perror("malloc"); display_word_wrap_7bit(string, display_width, left_margin); return; } memset(wide_string, 0, wide_string_buffer_size); if (mbstowcs(wide_string, string, 1 + wide_char_count) == (size_t) -1) { debug("%s: %s: %s", "mbstowcs", string, strerror(errno)); free(wide_string); display_word_wrap_7bit(string, display_width, left_margin); return; } start_idx = 0; chars_remaining = wide_char_count; /*@-unrecog@ *//* splint seems unable to see the prototype for wcswidth(). */ while (chars_remaining > 0 && wcswidth(&(wide_string[start_idx]), chars_remaining) > display_width) { /*@+unrecog@ */ size_t next_idx; end_idx = start_idx + display_width; while ((end_idx > start_idx) && (!iswspace(wide_string[end_idx]))) end_idx--; if (end_idx == start_idx) { end_idx = start_idx + display_width; } else { end_idx++; } next_idx = end_idx; if (end_idx == start_idx) next_idx++; while (start_idx < end_idx && start_idx < wide_char_count) { char multi_byte_string[MB_CUR_MAX + 1]; /* flawfinder: ignore */ /* * flawfinder rationale: array is explicitly * cleared, large enough according to the wctomb() * manual, and we explicitly terminate the string. */ memset(multi_byte_string, 0, MB_CUR_MAX + 1); if (wctomb(multi_byte_string, wide_string[start_idx]) >= 0) { multi_byte_string[MB_CUR_MAX] = '\0'; printf("%s", multi_byte_string); } start_idx++; } start_idx = next_idx; if (start_idx < wide_char_count) printf("\n%*s", (int) left_margin, ""); chars_remaining = wide_char_count - start_idx; } while (start_idx < wide_char_count) { char multi_byte_string[MB_CUR_MAX + 1]; /* flawfinder: ignore */ /* flawfinder rationale as above. */ memset(multi_byte_string, 0, MB_CUR_MAX + 1); if (wctomb(multi_byte_string, wide_string[start_idx]) >= 0) { multi_byte_string[MB_CUR_MAX] = '\0'; printf("%s", multi_byte_string); } start_idx++; } free(wide_string); #else /* ! defined(ENABLE_NLS) && defined(HAVE_WCHAR_H) */ display_word_wrap_7bit(string, display_width, left_margin); #endif /* defined(ENABLE_NLS) && defined(HAVE_WCHAR_H) */ } /* * Structure holding the displayed descriptions of each option - the short * option such as "-s", its long counterpart such as "--size", the name of * its argument such as "SIZE", and the description of what the option does. * Any of them may be NULL. * * In the initialiser for this structure, translatable strings are wrapped * with N_() to indicate that they are to be translated into the operator's * language by this function at run-time. * * Note that "opt_short" and "opt_long" should never be marked as * translatable, as they must remain consistent across all locales. * * The list is terminated with a NULL opt_short value - to leave a gap, set * opt_short to an empty string instead. */ struct option_definition_s { /*@null@ */ const char *opt_short; /*@null@ */ const char *opt_long; /*@null@ *//*@observer@ */ const char *opt_argument; /*@null@ *//*@observer@ */ const char *opt_description; struct { /* * Structure holding the width, in display character * positions, of the individual parts of the description of * each option - calculated after initialisation, so that * translation can be performed. */ size_t opt_short; size_t opt_long; size_t opt_argument; size_t opt_description; } width; }; /* * Display command-line help. */ void display_help(void) { struct option_definition_s option_definitions[] = { { "-p", "--progress", NULL, N_("show progress bar"), { 0, 0, 0, 0} }, { "-t", "--timer", NULL, N_("show elapsed time"), { 0, 0, 0, 0} }, { "-e", "--eta", NULL, N_("show estimated time of arrival (completion)"), { 0, 0, 0, 0} }, { "-I", "--fineta", NULL, N_("show absolute estimated time of arrival (completion)"), { 0, 0, 0, 0} }, { "-r", "--rate", NULL, N_("show data transfer rate counter"), { 0, 0, 0, 0} }, { "-a", "--average-rate", NULL, N_("show data transfer average rate counter"), { 0, 0, 0, 0} }, { "-b", "--bytes", NULL, N_("show number of bytes transferred"), { 0, 0, 0, 0} }, { "-T", "--buffer-percent", NULL, N_("show percentage of transfer buffer in use"), { 0, 0, 0, 0} }, { "-A", "--last-written", _("NUM"), N_("show NUM bytes last written"), { 0, 0, 0, 0} }, { "-F", "--format", N_("FORMAT"), N_("set output format to FORMAT"), { 0, 0, 0, 0} }, { "-n", "--numeric", NULL, N_("output percentages, not visual information"), { 0, 0, 0, 0} }, { "-q", "--quiet", NULL, N_("do not output any transfer information at all"), { 0, 0, 0, 0} }, { "", NULL, NULL, NULL, { 0, 0, 0, 0} }, { "-8", "--bits", NULL, N_("show number of bits transferred"), { 0, 0, 0, 0} }, { "-k", "--si", NULL, N_("treat suffixes as multiples of 1000 rather than 1024"), { 0, 0, 0, 0} }, { "-W", "--wait", NULL, N_("display nothing until first byte transferred"), { 0, 0, 0, 0} }, { "-D", "--delay-start", N_("SEC"), N_("display nothing until SEC seconds have passed"), { 0, 0, 0, 0} }, { "-s", "--size", N_("SIZE"), N_("set estimated data size to SIZE bytes"), { 0, 0, 0, 0} }, { "-g", "--gauge", NULL, N_("if size unknown, show rate vs max rate"), { 0, 0, 0, 0} }, { "-l", "--line-mode", NULL, N_("count lines instead of bytes"), { 0, 0, 0, 0} }, { "-0", "--null", NULL, N_("lines are null-terminated"), { 0, 0, 0, 0} }, { "-i", "--interval", N_("SEC"), N_("update every SEC seconds"), { 0, 0, 0, 0} }, { "-m", "--average-rate-window", N_("SEC"), N_("compute average rate over past SEC seconds (default 30s)"), { 0, 0, 0, 0} }, { "-w", "--width", N_("WIDTH"), N_("assume terminal is WIDTH characters wide"), { 0, 0, 0, 0} }, { "-H", "--height", N_("HEIGHT"), N_("assume terminal is HEIGHT rows high"), { 0, 0, 0, 0} }, { "-N", "--name", N_("NAME"), N_("prefix visual information with NAME"), { 0, 0, 0, 0} }, { "-u", "--bar-style", N_("STYLE"), N_("set default bar style to NAME"), { 0, 0, 0, 0} }, { "-x", "--extra-display", N_("SPEC"), N_("also send progress to SPEC"), { 0, 0, 0, 0} }, { "-v", "--stats", NULL, N_("output transfer statistics at the end"), { 0, 0, 0, 0} }, { "-f", "--force", NULL, N_("output even if standard error is not a terminal"), { 0, 0, 0, 0} }, { "-c", "--cursor", NULL, N_("use cursor positioning escape sequences"), { 0, 0, 0, 0} }, { "", NULL, NULL, NULL, { 0, 0, 0, 0} }, { "-o", "--output", N_("FILE"), N_("write output to FILE instead of stdout"), { 0, 0, 0, 0} }, { "-L", "--rate-limit", N_("RATE"), N_("limit transfer to RATE bytes per second"), { 0, 0, 0, 0} }, { "-B", "--buffer-size", N_("BYTES"), N_("use a buffer size of BYTES"), { 0, 0, 0, 0} }, { "-C", "--no-splice", NULL, N_("never use splice(), always use read/write"), { 0, 0, 0, 0} }, { "-E", "--skip-errors", NULL, N_("skip read errors in input"), { 0, 0, 0, 0} }, { "-Z", "--error-skip-block", N_("BYTES"), N_("skip errors in BYTES blocks at a time"), { 0, 0, 0, 0} }, { "-S", "--stop-at-size", NULL, N_("stop after --size bytes have been transferred"), { 0, 0, 0, 0} }, { "-Y", "--sync", NULL, N_("flush cache to disk after every write"), { 0, 0, 0, 0} }, { "-K", "--direct-io", NULL, N_("use direct I/O to bypass cache"), { 0, 0, 0, 0} }, { "-O", "--sparse", NULL, N_("try to seek instead of writing null bytes"), { 0, 0, 0, 0} }, { "-X", "--discard", NULL, N_("discard input instead of writing to output"), { 0, 0, 0, 0} }, { "-U", "--store-and-forward", N_("FILE"), N_("write all input to FILE before writing to output"), { 0, 0, 0, 0} }, { "", NULL, NULL, NULL, { 0, 0, 0, 0} }, { "-d", "--watchfd", N_("PID[:FD]|=NAME|@LISTFILE"), N_("watch file FD opened by process PID"), { 0, 0, 0, 0} }, #ifdef PV_REMOTE_CONTROL { "-R", "--remote", N_("PID"), N_("update settings of process PID"), { 0, 0, 0, 0} }, { "-Q", "--query", N_("PID"), N_("show progress of process PID"), { 0, 0, 0, 0} }, #endif /* PV_REMOTE_CONTROL */ { "", NULL, NULL, NULL, { 0, 0, 0, 0} }, { "-P", "--pidfile", N_("FILE"), N_("save process ID in FILE"), { 0, 0, 0, 0} }, { "-h", "--help", NULL, N_("show this help and exit"), { 0, 0, 0, 0} }, { "-V", "--version", NULL, N_("show version information and exit"), { 0, 0, 0, 0} }, #ifdef ENABLE_DEBUGGING { "-!", "--debug", N_("FILE"), N_("write debug logs to FILE"), { 0, 0, 0, 0} }, #endif { NULL, NULL, NULL, NULL, { 0, 0, 0, 0} } }; unsigned int option_index; size_t widest_option_width = 0; size_t description_left_margin = 0; size_t min_description_width = 50; size_t right_margin = 77; const char *program_description; const char *bug_report_note; char *format_sequences = NULL; unsigned int terminal_width = 0, terminal_height = 0; pv_screensize(&terminal_width, &terminal_height); if (terminal_width > 5) { right_margin = (size_t) (terminal_width - 3); } /*@-formatconst@ */ /* * splint note: unavoidable use of %s in translated string. Should * be hard to exploit - the message catalogue would have to be * replaced or forced to load from another location. */ printf(_("Usage: %s [OPTION] [FILE]..."), PACKAGE_NAME); /*@+formatconst@ */ printf("\n"); /*@-mustfreefresh@ */ /* * splint note: the gettext calls made by _() cause memory leak * warnings, but in this case it's unavoidable, and mitigated by the * fact we only translate each string once. */ program_description = _("Concatenate FILE(s), or standard input, to standard output, with monitoring."); if (NULL != program_description) { display_word_wrap(program_description, right_margin, 0); printf("\n"); } printf("\n"); /* * Translate the help text, and calculate the displayed width of * each part of each option definition. The total display width of * the short option, long option, and option argument together form * the "option" width - we look for the widest one to calculate the * left margin for all of the descriptions to start at. */ for (option_index = 0; NULL != option_definitions[option_index].opt_short; option_index++) { struct option_definition_s *definition; size_t option_width; definition = &(option_definitions[option_index]); option_width = 0; definition->width.opt_short = display_width(definition->opt_short); definition->width.opt_long = 0; definition->width.opt_argument = 0; definition->width.opt_description = 0; if (NULL != definition->opt_long) { definition->width.opt_long = display_width(definition->opt_long); } if (NULL != definition->opt_argument) { /*@observer@ */ const char *translated; translated = _(definition->opt_argument); if (NULL != translated) { definition->opt_argument = translated; } definition->width.opt_argument = display_width(definition->opt_argument); } if (NULL != definition->opt_description) { /*@observer@ */ const char *translated; translated = _(definition->opt_description); if (NULL != translated) { definition->opt_description = translated; } definition->width.opt_description = display_width(definition->opt_description); } /* * The option_width is padded with a left margin of 2 * spaces, a ", " between the short and long options, a * space between the long option and the argument, and two * spaces after the argument: * * " , " * * If we don't have getopt_long() then ", " is omitted. */ option_width += 2 + definition->width.opt_short; /* " short" */ #ifdef HAVE_GETOPT_LONG option_width += 2 + definition->width.opt_long; /* ", " */ #endif option_width += 1 + definition->width.opt_argument; /* " ARG" */ option_width += 2; /* final 2 spaces */ if (option_width > widest_option_width) { widest_option_width = option_width; } } /* * Set the left margin for the option descriptions, based on the * widest option width, or (right margin - min_description_width), * whichever is less, so that there is always room for * "min_description_width" characters of description. */ description_left_margin = right_margin - min_description_width; if (widest_option_width < description_left_margin) { description_left_margin = widest_option_width; } debug("%s: description_left_margin=%d, widest_option_width=%d, right_margin=%d", "help display", (int) description_left_margin, (int) widest_option_width, (int) right_margin); /* * Display each of the option definitions, word wrapping the * descriptions. */ for (option_index = 0; NULL != option_definitions[option_index].opt_short; option_index++) { struct option_definition_s *definition; size_t option_width; definition = &(option_definitions[option_index]); option_width = 0; if (definition->width.opt_short > 0 && NULL != definition->opt_short) { printf(" %s", definition->opt_short); option_width += 2 + definition->width.opt_short; } #ifdef HAVE_GETOPT_LONG if (definition->width.opt_long > 0 && NULL != definition->opt_long) { printf(", %s", definition->opt_long); option_width += 2 + definition->width.opt_long; } #endif if (definition->width.opt_argument > 0 && NULL != definition->opt_argument) { printf(" %s", definition->opt_argument); option_width += 1 + definition->width.opt_argument; } /* Just start a new line if there's no description. */ if ((0 == definition->width.opt_description) || (NULL == definition->opt_description)) { printf("\n"); continue; } /* * If the option is too wide, start a new line for the * description. In both cases, pad with spaces up to the * description left margin. */ if (option_width > description_left_margin) { printf("\n%*s", (int) description_left_margin, ""); } else if (option_width < description_left_margin) { printf("%*s", (int) (description_left_margin - option_width), ""); } /* Output the description, word wrapped. */ display_word_wrap(definition->opt_description, right_margin - description_left_margin, description_left_margin); printf("\n"); } format_sequences = pv_format_sequences(); if (NULL != format_sequences) { const char *format_sequence_header; format_sequence_header = _("Supported format sequences:"); if (NULL != format_sequence_header) { printf("\n"); display_word_wrap(format_sequence_header, right_margin, 0); printf("\n"); } printf("\n "); display_word_wrap(format_sequences, right_margin, 2); free(format_sequences); printf("\n"); } printf("\n"); bug_report_note = _("Please report any bugs to: %s"); if (NULL != bug_report_note) { /*@-formatconst@ */ /* * splint note: see earlier "formatconst" note. * flawfinder: same reason. */ printf(bug_report_note, PACKAGE_BUGREPORT); /* flawfinder: ignore */ /*@+formatconst@ */ } printf("\n"); }