diff --git a/man/journalctl.xml b/man/journalctl.xml
index 70d8508cf0c..d6f22789839 100644
--- a/man/journalctl.xml
+++ b/man/journalctl.xml
@@ -371,7 +371,8 @@
sensitive. This can be overridden with the option, see
below.
- When used with , is implied.
+ When used with (not prefixed with +),
+ is implied.
@@ -552,12 +553,13 @@
- Show the most recent journal events and limit the number of events shown. If
- is used, this option is implied. The argument is a positive integer or
- all to disable line limiting. The default value is 10 if no argument is
- given.
+ Show the most recent journal events and limit the number of events shown. The argument
+ is a positive integer or all to disable the limit. Additionally, if the number is
+ prefixed with +, the oldest journal events are used instead. The default value is
+ 10 if no argument is given.
- When used with , is implied.
+ If is used, this option is implied. When not prefixed with +
+ and used with , is implied.
diff --git a/src/journal/journalctl.c b/src/journal/journalctl.c
index 0e21f89da1c..a4e34fa0b12 100644
--- a/src/journal/journalctl.c
+++ b/src/journal/journalctl.c
@@ -95,6 +95,7 @@ static bool arg_full = true;
static bool arg_all = false;
static PagerFlags arg_pager_flags = 0;
static int arg_lines = ARG_LINES_DEFAULT;
+static int arg_lines_oldest = false;
static bool arg_no_tail = false;
static bool arg_truncate_newline = false;
static bool arg_quiet = false;
@@ -298,6 +299,44 @@ static int parse_boot_descriptor(const char *x, sd_id128_t *boot_id, int *offset
return 1;
}
+static int parse_lines(const char *arg, bool graceful) {
+ const char *l;
+ int n, r;
+
+ assert(arg || graceful);
+
+ if (!arg)
+ goto default_noarg;
+
+ if (streq(arg, "all")) {
+ arg_lines = ARG_LINES_ALL;
+ return 1;
+ }
+
+ l = startswith(arg, "+");
+
+ r = safe_atoi(l ?: arg, &n);
+ if (r < 0 || n < 0) {
+ if (graceful)
+ goto default_noarg;
+
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse --lines='%s'.", arg);
+ }
+
+ arg_lines = n;
+ arg_lines_oldest = !!l;
+
+ return 1;
+
+default_noarg:
+ arg_lines = 10;
+ return 0;
+}
+
+static bool arg_lines_needs_seek_end(void) {
+ return arg_lines >= 0 && !arg_lines_oldest;
+}
+
static int help_facilities(void) {
if (!arg_quiet)
puts("Available facilities:");
@@ -358,7 +397,7 @@ static int help(void) {
" json, json-pretty, json-sse, json-seq, cat,\n"
" with-unit)\n"
" --output-fields=LIST Select fields to print in verbose/export/json modes\n"
- " -n --lines[=INTEGER] Number of journal entries to show\n"
+ " -n --lines[=[+]INTEGER] Number of journal entries to show\n"
" -r --reverse Show the newest entries first\n"
" --show-cursor Print the cursor after all the entries\n"
" --utc Express time in Coordinated Universal Time (UTC)\n"
@@ -592,33 +631,11 @@ static int parse_argv(int argc, char *argv[]) {
break;
case 'n':
- if (optarg) {
- if (streq(optarg, "all"))
- arg_lines = ARG_LINES_ALL;
- else {
- r = safe_atoi(optarg, &arg_lines);
- if (r < 0 || arg_lines < 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse lines '%s'", optarg);
- }
- } else {
- arg_lines = 10;
-
- /* Hmm, no argument? Maybe the next
- * word on the command line is
- * supposed to be the argument? Let's
- * see if there is one, and is
- * parsable. */
- if (optind < argc) {
- int n;
- if (streq(argv[optind], "all")) {
- arg_lines = ARG_LINES_ALL;
- optind++;
- } else if (safe_atoi(argv[optind], &n) >= 0 && n >= 0) {
- arg_lines = n;
- optind++;
- }
- }
- }
+ r = parse_lines(optarg ?: optind < argc ? argv[optind] : NULL, !optarg);
+ if (r < 0)
+ return r;
+ if (r > 0 && !optarg)
+ optind++;
break;
@@ -1083,7 +1100,11 @@ static int parse_argv(int argc, char *argv[]) {
if (arg_follow && arg_reverse)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Please specify either --reverse= or --follow=, not both.");
+ "Please specify either --reverse or --follow, not both.");
+
+ if (arg_lines >= 0 && arg_lines_oldest && (arg_reverse || arg_follow))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "--lines=+N is unsupported when --reverse or --follow is specified.");
if (!IN_SET(arg_action, ACTION_SHOW, ACTION_DUMP_CATALOG, ACTION_LIST_CATALOG) && optind < argc)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
@@ -1110,11 +1131,12 @@ static int parse_argv(int argc, char *argv[]) {
if (r < 0)
return r;
- /* When --grep is used along with --lines, we don't know how many lines we can print.
- * So we search backwards and count until enough lines have been printed or we hit the head.
+ /* When --grep is used along with --lines without '+', i.e. when we start from the end of the
+ * journal, we don't know how many lines we can print. So we search backwards and count until
+ * enough lines have been printed or we hit the head.
* An exception is that --follow might set arg_lines, so let's not imply --reverse
* if that is specified. */
- if (arg_lines >= 0 && !arg_follow)
+ if (arg_lines_needs_seek_end() && !arg_follow)
arg_reverse = true;
}
@@ -2667,8 +2689,8 @@ static int run(int argc, char *argv[]) {
arg_lines = 0;
}
- } else if (arg_until_set && (arg_reverse || arg_lines >= 0)) {
- /* If both --until and any of --reverse and --lines is specified, things get
+ } else if (arg_until_set && (arg_reverse || arg_lines_needs_seek_end())) {
+ /* If both --until and any of --reverse and --lines=N is specified, things get
* a little tricky. We seek to the place of --until first. If only --reverse or
* --reverse and --lines is specified, we search backwards and let the output
* counter handle --lines for us. If only --lines is used, we just jump backwards
@@ -2680,7 +2702,7 @@ static int run(int argc, char *argv[]) {
if (arg_reverse)
r = sd_journal_previous(j);
- else /* arg_lines >= 0 */
+ else /* arg_lines_needs_seek_end */
r = sd_journal_previous_skip(j, arg_lines);
} else if (arg_reverse) {
@@ -2690,7 +2712,7 @@ static int run(int argc, char *argv[]) {
r = sd_journal_previous(j);
- } else if (arg_lines >= 0) {
+ } else if (arg_lines_needs_seek_end()) {
r = sd_journal_seek_tail(j);
if (r < 0)
return log_error_errno(r, "Failed to seek to tail: %m");