Use JSON output if --version (-V) is repeated (client side only).
authorWayne Davison <wayne@opencoder.net>
Sat, 10 Sep 2022 18:07:01 +0000 (11:07 -0700)
committerWayne Davison <wayne@opencoder.net>
Sat, 10 Sep 2022 20:14:42 +0000 (13:14 -0700)
NEWS.md
options.c
rsync.1.md
support/json-rsync-version [new file with mode: 0755]
usage.c

diff --git a/NEWS.md b/NEWS.md
index 3535e21b6a96ad47c2d2f51fcc7065abea79a1e5..81d66b63c94a5c23e004cb7295bf3e7421d3ccd9 100644 (file)
--- a/NEWS.md
+++ b/NEWS.md
   overly-long checksums are at the lowest priority in the normal checksum
   negotation list.
 
+- If the `--version` option is repeated (e.g. `-VV`) then the information is
+  output in a (still human-readable) JSON format (client side only).
+
+- The script `support/json-rsync-version` is available to get the JSON style
+  version output from any rsync.  The script accepts the version output on
+  stdin **or** the name of an rsync to run as an arg.  If the text isn't
+  already in JSON format, the text is translated into equivalent JSON.
+
 ### PACKAGING RELATED:
 
 - The checksum code now uses openssl's EVP methods, which gets rid of various
index 3f8d5d08bbc5649c5539fe61faaca5a94972116e..d38bbe8db344fceb3f068b9f8b35067ec74d8624 100644 (file)
--- a/options.c
+++ b/options.c
@@ -1926,7 +1926,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)
                saw_stderr_opt = 1;
 
        if (version_opt_cnt) {
-               print_rsync_version(FINFO);
+               print_rsync_version(version_opt_cnt > 1 && !am_server ? FNONE : FINFO);
                exit_cleanup(0);
        }
 
index bd9182a71e86a8d902c2af84ded0c3fd1a7c37dc..7271839d5690ff5be0c218585ca221caa14eea51 100644 (file)
@@ -580,11 +580,14 @@ expand it.
 
 0.  `--version`, `-V`
 
-    Print the rsync version plus other info and exit.
-
-    The output includes the default list of checksum algorithms, the default
-    list of compression algorithms, a list of compiled-in capabilities, a link
-    to the rsync web site, and some license/copyright info.
+    Print the rsync version plus other info and exit.  When repeated, the
+    information is output is a JSON format that is still hum-readable (client
+    side only).
+
+    The output includes a list of compiled-in capabilities, a list of
+    optimizations, the default list of checksum algorithms, the default list of
+    compression algorithms, the default list of daemon auth digests, a link to
+    the rsync web site, and a few other items.
 
 0.  `--verbose`, `-v`
 
diff --git a/support/json-rsync-version b/support/json-rsync-version
new file mode 100755 (executable)
index 0000000..79050e4
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/python3
+
+import sys, re, argparse, subprocess, json
+
+def main():
+    if not args.rsync or args.rsync == '-':
+        ver_out = sys.stdin.read().strip()
+    else:
+        ver_out = subprocess.check_output([args.rsync, '--version', '--version'], encoding='utf-8').strip()
+    if ver_out.startswith('{'):
+        print(ver_out)
+        return
+    info = { }
+    for line in ver_out.splitlines():
+        if line.startswith('rsync '):
+            prog, vstr, ver, pstr, vstr2, proto = line.split()
+            info['program'] = prog
+            if ver.startswith('v'):
+                ver = ver[1:]
+            info[vstr] = ver
+            if '.' not in proto:
+                proto += '.0'
+            else:
+                proto = proto.replace('.PR', '.')
+            info[pstr] = proto
+        elif line.startswith('Copyright '):
+            info['copyright'] = line[10:]
+        elif line.startswith('Web site: '):
+            info['url'] = line[10:]
+        elif line.startswith('  '):
+            if not saw_comma and ',' in line:
+                saw_comma = True
+            if saw_comma:
+                lst = line.strip(' ,').split(', ')
+            else:
+                lst = [ x for x in line.split() if not x.startswith('(') ]
+            info[sect_name] += lst
+        elif line == '':
+            break
+        else:
+            sect_name = line.strip(" \n:").replace(' ', '_').lower()
+            info[sect_name] = [ ]
+            saw_comma = False
+    for chk in 'checksum_list compress_list daemon_auth_list'.split():
+        if chk not in info:
+            info[chk] = [ ]
+    info['license'] = 'GPL3'
+    info['caveat'] = 'rsync comes with ABSOLUTELY NO WARRANTY'
+    print(json.dumps(info))
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(description="Output rsync's version data in JSON format, even if the rsync doesn't support a native json-output method.", add_help=False)
+    parser.add_argument('rsync', nargs='?', help="Specify an rsync command to run. Otherwise stdin is consumed.")
+    parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
+    args = parser.parse_args()
+    main()
+
+# vim: sw=4 et
diff --git a/usage.c b/usage.c
index 253f6660650d869e9f40b94b80b379a13786e032..dc66288f910832130d5d09fd895d01aff69b8a44 100644 (file)
--- a/usage.c
+++ b/usage.c
@@ -22,6 +22,7 @@
 #include "latest-year.h"
 #include "git-version.h"
 #include "default-cvsignore.h"
+#include "itypes.h"
 
 extern struct name_num_obj valid_checksums, valid_compressions, valid_auth_checksums;
 
@@ -36,7 +37,8 @@ static char *istring(const char *fmt, int val)
 static void print_info_flags(enum logcode f)
 {
        STRUCT_STAT *dumstat;
-       char line_buf[75];
+       BOOL as_json = f == FNONE ? 1 : 0; /* We use 1 == first attribute, 2 == need closing array */
+       char line_buf[75], *quot = as_json ? "\"" : "";
        int line_len, j;
        char *info_flags[] = {
 
@@ -163,50 +165,115 @@ static void print_info_flags(enum logcode f)
 
        for (line_len = 0, j = 0; ; j++) {
                char *str = info_flags[j], *next_nfo = str ? info_flags[j+1] : NULL;
-               int str_len = str && *str != '*' ? strlen(str) : 1000;
+               int str_len = str && *str != '*' ? strlen(str) + (as_json ? 2 : 0) : 1000;
                int need_comma = next_nfo && *next_nfo != '*' ? 1 : 0;
                if (line_len && line_len + 1 + str_len + need_comma >= (int)sizeof line_buf) {
-                       rprintf(f, "   %s\n", line_buf);
+                       if (as_json)
+                               printf("   %s\n", line_buf);
+                       else
+                               rprintf(f, "   %s\n", line_buf);
                        line_len = 0;
                }
                if (!str)
                        break;
                if (*str == '*') {
-                       rprintf(f, "%s:\n", str+1);
+                       if (as_json) {
+                               if (as_json == 2)
+                                       printf("  ]");
+                               else
+                                       as_json = 2;
+                               printf(",\n  \"%c%s\": [\n", toLower(str+1), str+2);
+                       } else
+                               rprintf(f, "%s:\n", str+1);
                        continue;
                }
-               line_len += snprintf(line_buf+line_len, sizeof line_buf - line_len, " %s%s", str, need_comma ? "," : "");
+               line_len += snprintf(line_buf+line_len, sizeof line_buf - line_len,
+                                    " %s%s%s%s", quot, str, quot, need_comma ? "," : "");
        }
+       if (as_json == 2)
+               printf("  ]");
 }
 
-void print_rsync_version(enum logcode f)
+static void output_nno_list(enum logcode f, const char *name, struct name_num_obj *nno)
 {
-       char tmpbuf[256], *subprotocol = "";
+       char namebuf[64], tmpbuf[256];
+       char *tok, *next_tok, *comma = ",";
+       char *cp;
+
+       /* Using '(' ensures that we get a trailing "none" but also includes aliases. */
+       get_default_nno_list(nno, tmpbuf, sizeof tmpbuf - 1, '(');
+       if (f != FNONE) {
+               rprintf(f, "%s:\n", name);
+               rprintf(f, "    %s\n", tmpbuf);
+               return;
+       }
+
+       strlcpy(namebuf, name, sizeof namebuf);
+       for (cp = namebuf; *cp; cp++) {
+               if (*cp == ' ')
+                       *cp = '_';
+               else if (isUpper(cp))
+                       *cp = toLower(cp);
+       }
+
+       printf(",\n  \"%s\": [\n   ", namebuf);
+
+       for (tok = strtok(tmpbuf, " "); tok; tok = next_tok) {
+               next_tok = strtok(NULL, " ");
+               if (*tok != '(') /* Ignore the alises in the JSON output */
+                       printf(" \"%s\"%s", tok, comma + (next_tok ? 0 : 1));
+       }
+
+       printf("\n  ]");
+}
 
+/* A request of f == FNONE wants json on stdout. */
+void print_rsync_version(enum logcode f)
+{
+       char copyright[] = "(C) 1996-" LATEST_YEAR " by Andrew Tridgell, Wayne Davison, and others.";
+       char url[] = "https://rsync.samba.org/";
+       BOOL first_line = 1;
+
+#define json_line(name, value) \
+       do { \
+               printf("%c\n  \"%s\": \"%s\"", first_line ? '{' : ',', name, value); \
+               first_line = 0; \
+       } while (0)
+
+       if (f == FNONE) {
+               char verbuf[32];
+               json_line("program", RSYNC_NAME);
+               json_line("version", rsync_version());
+               snprintf(verbuf, sizeof verbuf, "%d.%d", PROTOCOL_VERSION, SUBPROTOCOL_VERSION);
+               json_line("protocol", verbuf);
+               json_line("copyright", copyright);
+               json_line("url", url);
+       } else {
 #if SUBPROTOCOL_VERSION != 0
-       subprotocol = istring(".PR%d", SUBPROTOCOL_VERSION);
+               char *subprotocol = istring(".PR%d", SUBPROTOCOL_VERSION);
+#else
+               char *subprotocol = "";
 #endif
-       rprintf(f, "%s  version %s  protocol version %d%s\n",
-               RSYNC_NAME, rsync_version(), PROTOCOL_VERSION, subprotocol);
-
-       rprintf(f, "Copyright (C) 1996-" LATEST_YEAR " by Andrew Tridgell, Wayne Davison, and others.\n");
-       rprintf(f, "Web site: https://rsync.samba.org/\n");
+               rprintf(f, "%s  version %s  protocol version %d%s\n",
+                       RSYNC_NAME, rsync_version(), PROTOCOL_VERSION, subprotocol);
+               rprintf(f, "Copyright %s\n", copyright);
+               rprintf(f, "Web site: %s\n", url);
+       }
 
        print_info_flags(f);
 
        init_checksum_choices();
 
-       rprintf(f, "Checksum list:\n");
-       get_default_nno_list(&valid_checksums, tmpbuf, sizeof tmpbuf, '(');
-       rprintf(f, "    %s\n", tmpbuf);
+       output_nno_list(f, "Checksum list", &valid_checksums);
+       output_nno_list(f, "Compress list", &valid_compressions);
+       output_nno_list(f, "Daemon auth list", &valid_auth_checksums);
 
-       rprintf(f, "Compress list:\n");
-       get_default_nno_list(&valid_compressions, tmpbuf, sizeof tmpbuf, '(');
-       rprintf(f, "    %s\n", tmpbuf);
-
-       rprintf(f, "Daemon auth list:\n");
-       get_default_nno_list(&valid_auth_checksums, tmpbuf, sizeof tmpbuf, '(');
-       rprintf(f, "    %s\n", tmpbuf);
+       if (f == FNONE) {
+               json_line("license", "GPL3");
+               json_line("caveat", "rsync comes with ABSOLUTELY NO WARRANTY");
+               printf("\n}\n");
+               return;
+       }
 
 #ifdef MAINTAINER_MODE
        rprintf(f, "Panic Action: \"%s\"\n", get_panic_action());
@@ -268,11 +335,13 @@ void daemon_usage(enum logcode F)
 
 const char *rsync_version(void)
 {
+       char *ver;
 #ifdef RSYNC_GITVER
-       return RSYNC_GITVER;
+       ver = RSYNC_GITVER;
 #else
-       return RSYNC_VERSION;
+       ver = RSYNC_VERSION;
 #endif
+       return *ver == 'v' ? ver+1 : ver;
 }
 
 const char *default_cvsignore(void)