diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml
index 97290d479b..8061743c56 100644
--- a/man/systemd-analyze.xml
+++ b/man/systemd-analyze.xml
@@ -118,6 +118,14 @@
cat-configNAME|PATH
+
+ systemd-analyze
+ OPTIONS
+ compare-versions
+ VERSION1
+ OP
+ VERSION2
+ systemd-analyzeOPTIONS
@@ -547,6 +555,52 @@ NAutoVTs=8
+
+ systemd-analyze compare-versions
+ VERSION1
+ OP
+ VERSION2
+
+ This command has two distinct modes of operation, depending on whether the operator
+ OP is specified.
+
+ In the first mode — when OP is not specified — it will compare the two
+ version strings and print either VERSION1 <
+ VERSION2, or VERSION1 ==
+ VERSION2, or VERSION1 >
+ VERSION2 as appropriate.
+
+ The exit status is 0 if the versions are equal, 11 if
+ the version of the right is smaller, and 12 if the version of the left is
+ smaller. (This matches the convention used by rpmdev-vercmp.)
+
+ In the second mode — when OP is specified — it will compare the two
+ version strings using the operation OP and return 0
+ (success) if they condition is satisfied, and 1 (failure)
+ otherwise. OP may be lt, le,
+ eq, ne, ge, gt. In this
+ mode, no output is printed.
+ (This matches the convention used by
+ dpkg1
+ .)
+
+
+ Compare versions of a package
+
+
+$ systemd-analyze compare-versions systemd-250~rc1.fc36.aarch64 systemd-251.fc36.aarch64
+systemd-250~rc1.fc36.aarch64 < systemd-251.fc36.aarch64
+$ echo $?
+12
+
+$ systemd-analyze compare-versions 1 lt 2; echo $?
+0
+$ systemd-analyze compare-versions 1 ge 2; echo $?
+1
+
+
+
+
systemd-analyze verify FILE...
@@ -1197,8 +1251,12 @@ $ systemd-analyze verify /tmp/source:alias.service
Exit status
- On success, 0 is returned, a non-zero failure code
- otherwise.
+ For most commands, 0 is returned on success, and a non-zero failure code otherwise.
+
+ With the verb compare-versions, in the two-argument form,
+ 12, 0, 11 is returned if the second
+ version string is respectively larger, equal, or smaller to the first. In the three-argument form,
+ 0 or 1 if the condition is respectively true or false.
diff --git a/src/analyze/analyze-compare-versions.c b/src/analyze/analyze-compare-versions.c
new file mode 100644
index 0000000000..9545326fa9
--- /dev/null
+++ b/src/analyze/analyze-compare-versions.c
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "analyze-compare-versions.h"
+#include "macro.h"
+#include "string-util.h"
+#include "strv.h"
+
+int verb_compare_versions(int argc, char *argv[], void *userdata) {
+ int r;
+
+ assert(argc == 3 || argc == 4);
+ assert(argv);
+
+ if (argc == 3) {
+ r = strverscmp_improved(ASSERT_PTR(argv[1]), ASSERT_PTR(argv[2]));
+ printf("%s %s %s\n",
+ argv[1],
+ r < 0 ? "<" : r > 0 ? ">" : "==",
+ argv[2]);
+
+ /* This matches the exit convention used by rpmdev-vercmp.
+ * We don't use named values because 11 and 12 don't have names. */
+ return r < 0 ? 12 : r > 0 ? 11 : 0;
+
+ } else {
+ const char *op = ASSERT_PTR(argv[2]);
+
+ r = strverscmp_improved(ASSERT_PTR(argv[1]), ASSERT_PTR(argv[3]));
+
+ if (STR_IN_SET(op, "lt", "<"))
+ return r < 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+ if (STR_IN_SET(op, "le", "<="))
+ return r <= 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+ if (STR_IN_SET(op, "eq", "=="))
+ return r == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+ if (STR_IN_SET(op, "ne", "!="))
+ return r != 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+ if (STR_IN_SET(op, "ge", ">="))
+ return r >= 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+ if (STR_IN_SET(op, "gt", ">"))
+ return r > 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown operator \"%s\".", op);
+ }
+}
diff --git a/src/analyze/analyze-compare-versions.h b/src/analyze/analyze-compare-versions.h
new file mode 100644
index 0000000000..ac90ede2f0
--- /dev/null
+++ b/src/analyze/analyze-compare-versions.h
@@ -0,0 +1,3 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+int verb_compare_versions(int argc, char *argv[], void *userdata);
diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c
index 2935ecea7a..4968946963 100644
--- a/src/analyze/analyze.c
+++ b/src/analyze/analyze.c
@@ -35,6 +35,7 @@
#include "analyze-timestamp.h"
#include "analyze-unit-files.h"
#include "analyze-unit-paths.h"
+#include "analyze-compare-versions.h"
#include "analyze-verify.h"
#include "bus-error.h"
#include "bus-locator.h"
@@ -199,6 +200,8 @@ static int help(int argc, char *argv[], void *userdata) {
" syscall-filter [NAME...] List syscalls in seccomp filters\n"
" filesystems [NAME...] List known filesystems\n"
" condition CONDITION... Evaluate conditions and asserts\n"
+ " compare-versions VERSION1 [OP] VERSION2\n"
+ " Compare two version strings\n"
" verify FILE... Check unit files for correctness\n"
" calendar SPEC... Validate repetitive calendar time\n"
" events\n"
@@ -564,6 +567,7 @@ static int run(int argc, char *argv[]) {
{ "capability", VERB_ANY, VERB_ANY, 0, verb_capabilities },
{ "filesystems", VERB_ANY, VERB_ANY, 0, verb_filesystems },
{ "condition", VERB_ANY, VERB_ANY, 0, verb_condition },
+ { "compare-versions", 3, 4, 0, verb_compare_versions },
{ "verify", 2, VERB_ANY, 0, verb_verify },
{ "calendar", 2, VERB_ANY, 0, verb_calendar },
{ "timestamp", 2, VERB_ANY, 0, verb_timestamp },
diff --git a/src/analyze/meson.build b/src/analyze/meson.build
index f0cfbb195e..24ef94149c 100644
--- a/src/analyze/meson.build
+++ b/src/analyze/meson.build
@@ -9,6 +9,8 @@ systemd_analyze_sources = files(
'analyze-capability.h',
'analyze-cat-config.c',
'analyze-cat-config.h',
+ 'analyze-compare-versions.c',
+ 'analyze-compare-versions.h',
'analyze-condition.c',
'analyze-condition.h',
'analyze-critical-chain.c',