From dd7f907040b7894fa6224bf53450336f0bb42813 Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Tue, 21 Mar 2023 10:13:47 +0100 Subject: [PATCH 1/9] human-readable: calculate numbers in a loop This drops a lot of `else if` blocks and extends units by "E" (Exa), which is 2^60 and thus the last to fit into int64. Co-authored-by: Zen Dodd --- lib/compat.c | 40 ++++++++++++++++++++-------------------- rsync.1.md | 6 +++--- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/compat.c b/lib/compat.c index 513d79b23..71738afe2 100644 --- a/lib/compat.c +++ b/lib/compat.c @@ -173,34 +173,34 @@ char *do_big_num(int64 num, int human_flag, const char *fract) static unsigned int n; char *s; int len, negated; + uint64_t abs_num; if (human_flag && !number_separator) (void)get_number_separator(); n = (n + 1) % (sizeof bufs / sizeof bufs[0]); + abs_num = num < 0 ? 0 - (uint64_t)num : (uint64_t)num; if (human_flag > 1) { - int mult = human_flag == 2 ? 1000 : 1024; - if (num >= mult || num <= -mult) { - double dnum = (double)num / mult; - char units; - if (num < 0) - dnum = -dnum; - if (dnum < mult) - units = 'K'; - else if ((dnum /= mult) < mult) - units = 'M'; - else if ((dnum /= mult) < mult) - units = 'G'; - else if ((dnum /= mult) < mult) - units = 'T'; - else { - dnum /= mult; - units = 'P'; + unsigned int mult = human_flag == 2 ? 1000 : 1024; + + if (abs_num >= mult) { + const char* units = " KMGTPE"; + uint64_t powi = 1; + + for (;;) { + if (abs_num / mult < powi) + break; + + if (units[1] == '\0') + break; + + powi *= mult; + ++units; } - if (num < 0) - dnum = -dnum; - snprintf(bufs[n], sizeof bufs[0], "%.2f%c", dnum, units); + + snprintf(bufs[n], sizeof bufs[0], "%.2f%c", + (double) num / powi, *units); return bufs[n]; } } diff --git a/rsync.1.md b/rsync.1.md index fdf0b2e95..30bf59d51 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -3352,9 +3352,9 @@ expand it. digits) by specifying the `--no-human-readable` (`--no-h`) option. The unit letters that are appended in levels 2 and 3 are: `K` (kilo), `M` - (mega), `G` (giga), `T` (tera), or `P` (peta). For example, a 1234567-byte - file would output as 1.23M in level-2 (assuming that a period is your local - decimal point). + (mega), `G` (giga), `T` (tera), `P` (peta) or `E` (exa). For example, a + 1234567-byte file would output as 1.23M in level-2 (assuming that a + period is your local decimal point). Backward compatibility note: versions of rsync prior to 3.1.0 do not support human-readable level 1, and they default to level 0. Thus, From 0e5fedced876279d714a0d1e4c85702dcffe4d10 Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Tue, 9 Jun 2026 10:05:44 +0200 Subject: [PATCH 2/9] human-readable: do not add trailing space in output Co-authored-by: Zen Dodd --- lib/compat.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/compat.c b/lib/compat.c index 71738afe2..a7aa9325a 100644 --- a/lib/compat.c +++ b/lib/compat.c @@ -185,22 +185,24 @@ char *do_big_num(int64 num, int human_flag, const char *fract) unsigned int mult = human_flag == 2 ? 1000 : 1024; if (abs_num >= mult) { - const char* units = " KMGTPE"; + char units[] = "\0KMGTPE"; + char *unit = units; uint64_t powi = 1; for (;;) { if (abs_num / mult < powi) break; - if (units[1] == '\0') + if (unit[1] == '\0') break; powi *= mult; - ++units; + ++unit; } + unit[1] = '\0'; - snprintf(bufs[n], sizeof bufs[0], "%.2f%c", - (double) num / powi, *units); + snprintf(bufs[n], sizeof bufs[0], "%.2f%s", + (double) num / powi, unit); return bufs[n]; } } From 28410cc02463e0f9f1c71920c04564b94a5902f6 Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Tue, 21 Mar 2023 10:17:09 +0100 Subject: [PATCH 3/9] human-readable: use dynamic precision length Let's lower precision for huge numbers. The output used to be: 3.45M -> 46.73M -> 523.11M -> 1.24G -> ... With this change the code always gives the three most significant digits: 3.45M -> 46.7M -> 523M -> 1.24G -> ... Co-authored-by: Zen Dodd --- lib/compat.c | 11 +++++++++-- rsync.1.md | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/compat.c b/lib/compat.c index a7aa9325a..fdeb03102 100644 --- a/lib/compat.c +++ b/lib/compat.c @@ -188,6 +188,7 @@ char *do_big_num(int64 num, int human_flag, const char *fract) char units[] = "\0KMGTPE"; char *unit = units; uint64_t powi = 1; + unsigned int powj = 1, precision = 2; for (;;) { if (abs_num / mult < powi) @@ -201,8 +202,14 @@ char *do_big_num(int64 num, int human_flag, const char *fract) } unit[1] = '\0'; - snprintf(bufs[n], sizeof bufs[0], "%.2f%s", - (double) num / powi, unit); + for (; precision > 0; precision--) { + powj *= 10; + if (abs_num / powi < powj) + break; + } + + snprintf(bufs[n], sizeof bufs[0], "%.*f%s", + precision, (double) num / powi, unit); return bufs[n]; } } diff --git a/rsync.1.md b/rsync.1.md index 30bf59d51..a3a2b28e8 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -3354,7 +3354,9 @@ expand it. The unit letters that are appended in levels 2 and 3 are: `K` (kilo), `M` (mega), `G` (giga), `T` (tera), `P` (peta) or `E` (exa). For example, a 1234567-byte file would output as 1.23M in level-2 (assuming that a - period is your local decimal point). + period is your local decimal point). Dynamic precision is applied, so the + three most-significant digits are shown (for example: 3.45M -> 46.7M -> + 523M -> 1.24G -> ...). Backward compatibility note: versions of rsync prior to 3.1.0 do not support human-readable level 1, and they default to level 0. Thus, From 8b9946d2224d3fa9356013839c21cfeeece1f00b Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Wed, 22 Mar 2023 11:07:15 +0100 Subject: [PATCH 4/9] human-readable: also handle num < mult with the same code ... just make sure no precision is added. Co-authored-by: Zen Dodd --- lib/compat.c | 55 ++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/compat.c b/lib/compat.c index fdeb03102..1f00bbc54 100644 --- a/lib/compat.c +++ b/lib/compat.c @@ -184,34 +184,35 @@ char *do_big_num(int64 num, int human_flag, const char *fract) if (human_flag > 1) { unsigned int mult = human_flag == 2 ? 1000 : 1024; - if (abs_num >= mult) { - char units[] = "\0KMGTPE"; - char *unit = units; - uint64_t powi = 1; - unsigned int powj = 1, precision = 2; - - for (;;) { - if (abs_num / mult < powi) - break; - - if (unit[1] == '\0') - break; - - powi *= mult; - ++unit; - } - unit[1] = '\0'; - - for (; precision > 0; precision--) { - powj *= 10; - if (abs_num / powi < powj) - break; - } - - snprintf(bufs[n], sizeof bufs[0], "%.*f%s", - precision, (double) num / powi, unit); - return bufs[n]; + char units[] = "\0KMGTPE"; + char *unit = units; + uint64_t powi = 1; + unsigned int powj = 1, precision = 2; + + for (;;) { + if (abs_num / mult < powi) + break; + + if (unit[1] == '\0') + break; + + powi *= mult; + ++unit; } + unit[1] = '\0'; + + if (powi == 1) + precision = 0; + + for (; precision > 0; precision--) { + powj *= 10; + if (abs_num / powi < powj) + break; + } + + snprintf(bufs[n], sizeof bufs[0], "%.*f%s", precision, + (double) num / powi, unit); + return bufs[n]; } s = bufs[n] + sizeof bufs[0] - 1; From cfc82b2998a8ad6e66495779f3158667b619acc5 Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Thu, 23 Mar 2023 10:33:33 +0100 Subject: [PATCH 5/9] human-readable: add an "i" to unit to indicate binary (1024) base Co-authored-by: Zen Dodd --- lib/compat.c | 4 ++-- rsync.1.md | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/compat.c b/lib/compat.c index 1f00bbc54..c108bcbc1 100644 --- a/lib/compat.c +++ b/lib/compat.c @@ -210,8 +210,8 @@ char *do_big_num(int64 num, int human_flag, const char *fract) break; } - snprintf(bufs[n], sizeof bufs[0], "%.*f%s", precision, - (double) num / powi, unit); + snprintf(bufs[n], sizeof bufs[0], "%.*f%s%s", precision, + (double) num / powi, unit, *unit && mult == 1024 ? "i" : ""); return bufs[n]; } diff --git a/rsync.1.md b/rsync.1.md index a3a2b28e8..83f3b6799 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -3357,6 +3357,8 @@ expand it. period is your local decimal point). Dynamic precision is applied, so the three most-significant digits are shown (for example: 3.45M -> 46.7M -> 523M -> 1.24G -> ...). + Additionally an `i` is appended in level-3 to indicate the binary base. + The same file would output as 1.17Mi in level-3. Backward compatibility note: versions of rsync prior to 3.1.0 do not support human-readable level 1, and they default to level 0. Thus, From 3170615e8b29adecd370767f89a958d566bb24cc Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Tue, 9 Apr 2024 09:56:39 +0200 Subject: [PATCH 6/9] human-readable: also use it to format rate in progress Let's also simplify the code for rate in progress, and benefit from same functionality. Co-authored-by: Zen Dodd --- progress.c | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/progress.c b/progress.c index 87207fbfa..2e2ff891f 100644 --- a/progress.c +++ b/progress.c @@ -69,7 +69,6 @@ static unsigned long msdiff(struct timeval *t1, struct timeval *t2) static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_last) { char rembuf[64], eol[128]; - const char *units; unsigned long diff; double rate, remain; int pct; @@ -93,7 +92,7 @@ static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_l /* Compute stats based on the starting info. */ if (!ph_start.time.tv_sec || !(diff = msdiff(&ph_start.time, now))) diff = 1; - rate = (double) (ofs - ph_start.ofs) * 1000.0 / diff / 1024.0; + rate = (double) (ofs - ph_start.ofs) * 1000.0 / diff; /* Switch to total time taken for our last update. */ remain = (double) diff / 1000.0; } else { @@ -101,18 +100,8 @@ static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_l /* Compute stats based on recent progress. */ if (!(diff = msdiff(&ph_list[oldest_hpos].time, now))) diff = 1; - rate = (double) (ofs - ph_list[oldest_hpos].ofs) * 1000.0 / diff / 1024.0; - remain = rate ? (double) (size - ofs) / rate / 1000.0 : 0.0; - } - - if (rate > 1024*1024) { - rate /= 1024.0 * 1024.0; - units = "GB/s"; - } else if (rate > 1024) { - rate /= 1024.0; - units = "MB/s"; - } else { - units = "kB/s"; + rate = (double) (ofs - ph_list[oldest_hpos].ofs) * 1000.0 / diff; + remain = rate ? (double) (size - ofs) / rate : 0.0; } if (remain < 0 || remain > 9999.0 * 3600.0) @@ -126,8 +115,8 @@ static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_l output_needs_newline = 0; pct = ofs == size ? 100 : (int) (100.0 * ofs / size); - rprintf(FCLIENT, "\r%15s %3d%% %7.2f%s %s%s", - human_num(ofs), pct, rate, units, rembuf, eol); + rprintf(FCLIENT, "\r%15s %3d%% %7sB/s %s%s", + human_num(ofs), pct, human_num((int64)rate), rembuf, eol); if (!is_last && !quiet) { output_needs_newline = 1; rflush(FCLIENT); From 144f88bcb0d2d5f4af5e9add695b24e5ba117ba4 Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Tue, 9 Apr 2024 10:50:20 +0200 Subject: [PATCH 7/9] human-readable: append suffix "B" for byte to ofs... ... and also fix the formatting in man page. Co-authored-by: Zen Dodd --- progress.c | 2 +- rsync.1.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/progress.c b/progress.c index 2e2ff891f..97d0423dd 100644 --- a/progress.c +++ b/progress.c @@ -115,7 +115,7 @@ static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_l output_needs_newline = 0; pct = ofs == size ? 100 : (int) (100.0 * ofs / size); - rprintf(FCLIENT, "\r%15s %3d%% %7sB/s %s%s", + rprintf(FCLIENT, "\r%15sB %3d%% %7sB/s %s%s", human_num(ofs), pct, human_num((int64)rate), rembuf, eol); if (!is_last && !quiet) { output_needs_newline = 1; diff --git a/rsync.1.md b/rsync.1.md index 83f3b6799..899eceda5 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -3526,10 +3526,10 @@ expand it. While rsync is transferring a regular file, it updates a progress line that looks like this: - > 782448 63% 110.64kB/s 0:00:04 + > 782,448B 63% 113,295B/s 0:00:04 - In this example, the receiver has reconstructed 782448 bytes or 63% of the - sender's file, which is being reconstructed at a rate of 110.64 kilobytes + In this example, the receiver has reconstructed 782,448 bytes or 63% of the + sender's file, which is being reconstructed at a rate of 113,295 bytes per second, and the transfer will finish in 4 seconds if the current rate is maintained until the end. @@ -3543,11 +3543,11 @@ expand it. When the file transfer finishes, rsync replaces the progress line with a summary line that looks like this: - > 1,238,099 100% 146.38kB/s 0:00:08 (xfr#5, to-chk=169/396) + > 1,238,099B 100% 149,893B/s 0:00:08 (xfr#5, to-chk=169/396) In this example, the file was 1,238,099 bytes long in total, the average - rate of transfer for the whole file was 146.38 kilobytes per second over - the 8 seconds that it took to complete, it was the 5th transfer of a + rate of transfer for the whole file was 149,893 bytes per second over the + 8 seconds that it took to complete, it was the 5th transfer of a regular file during the current rsync session, and there are 169 more files for the receiver to check (to see if they are up-to-date or not) remaining out of the 396 total files in the file-list. From d23b0ec23029ff97f1b2be0a715feced29457464 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 11 Jun 2026 14:58:04 +1000 Subject: [PATCH 8/9] human-readable: unbreak for do_big_dnum() with fract --- lib/compat.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/compat.c b/lib/compat.c index c108bcbc1..cc50ab1a7 100644 --- a/lib/compat.c +++ b/lib/compat.c @@ -187,7 +187,6 @@ char *do_big_num(int64 num, int human_flag, const char *fract) char units[] = "\0KMGTPE"; char *unit = units; uint64_t powi = 1; - unsigned int powj = 1, precision = 2; for (;;) { if (abs_num / mult < powi) @@ -201,18 +200,19 @@ char *do_big_num(int64 num, int human_flag, const char *fract) } unit[1] = '\0'; - if (powi == 1) - precision = 0; + if (powi > 1) { + unsigned int powj = 1, precision = 2; - for (; precision > 0; precision--) { - powj *= 10; - if (abs_num / powi < powj) - break; - } + for (; precision > 0; precision--) { + powj *= 10; + if (abs_num / powi < powj) + break; + } - snprintf(bufs[n], sizeof bufs[0], "%.*f%s%s", precision, - (double) num / powi, unit, *unit && mult == 1024 ? "i" : ""); - return bufs[n]; + snprintf(bufs[n], sizeof bufs[0], "%.*f%s%s", precision, + (double) num / powi, unit, *unit && mult == 1024 ? "i" : ""); + return bufs[n]; + } } s = bufs[n] + sizeof bufs[0] - 1; From 00c8035624b38ec2beb52dd7e029994f7a7ac87c Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 11 Jun 2026 14:58:04 +1000 Subject: [PATCH 9/9] testsuite: cover human-readable output --- testsuite/output-options_test.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/testsuite/output-options_test.py b/testsuite/output-options_test.py index 61a4d4ba9..dbf140b09 100644 --- a/testsuite/output-options_test.py +++ b/testsuite/output-options_test.py @@ -95,8 +95,9 @@ def out(*args, want_rc=0, env=None, text=True): # --- --progress shows a percentage ------------------------------------------ rmtree(TODIR) p = out('-a', '--progress', f'{src}/', f'{TODIR}/') -if '100%' not in p.stdout: - test_fail(f"--progress did not show a percentage:\n{p.stdout}") +progress_re = r'[\d.,]+B\s+100%\s+[\d.,]+B/s\s+\d+:\d\d:\d\d' +if not re.search(progress_re, p.stdout): + test_fail(f"--progress did not show the expected final shape:\n{p.stdout}") # --- -h / --human-readable formats byte counts with a unit suffix ----------- # Without -h, --stats prints grouped digits ("50,000 bytes"); with -h it uses a @@ -114,6 +115,28 @@ def out(*args, want_rc=0, env=None, text=True): if re.search(suffix_re, plain): test_fail(f"--stats without -h unexpectedly used a unit suffix:\n{plain}") +rmtree(TODIR) +human_binary = out('-a', '-hh', '--stats', f'{src}/', f'{TODIR}/').stdout +if 'Total file size: 48.8Ki bytes' not in human_binary: + test_fail(f"-hh did not use dynamic binary-unit precision:\n{human_binary}") + +rmtree(src) +rmtree(TODIR) +makepath(src) +make_data_file(src / 'threshold', 1024) +threshold = out('-a', '-hh', '--stats', f'{src}/', f'{TODIR}/').stdout +if 'Total file size: 1.00Ki bytes' not in threshold: + test_fail(f"-hh did not show an exact-threshold binary unit:\n{threshold}") + +rmtree(src) +rmtree(TODIR) +makepath(src) +make_data_file(src / 'below-threshold', 1000) +below_threshold = out('-a', '-hh', '--stats', f'{src}/', f'{TODIR}/').stdout +if 'Total file size: 1,000 bytes' not in below_threshold: + test_fail("-hh should preserve raw byte formatting below the binary-unit " + f"threshold:\n{below_threshold}") + # --- -8 / --8-bit-output leaves high-bit filename bytes unescaped ------------ # rsync escapes non-printable name bytes as \#NNN; -8 prints 8-bit bytes raw. # This needs a filename containing a high-bit byte and a C locale (where such a