From c4c978a07fede2d2be3c401c4b64b6a12091f34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 12 Jan 2018 05:47:17 +0100 Subject: [PATCH 1/4] meson: detect pcre2 dep --- meson.build | 12 ++++++++++++ meson_options.txt | 2 ++ src/basic/build.h | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/meson.build b/meson.build index 924274de63..686ec6ab84 100644 --- a/meson.build +++ b/meson.build @@ -1064,6 +1064,17 @@ else endif conf.set10('HAVE_XKBCOMMON', have) +want_pcre2 = get_option('pcre2') +if want_pcre2 != 'false' + libpcre2 = dependency('libpcre2-8', + required : want_pcre2 == 'true') + have = libpcre2.found() +else + have = false + libpcre2 = [] +endif +conf.set10('HAVE_PCRE2', have) + want_glib = get_option('glib') if want_glib != 'false' libglib = dependency('glib-2.0', @@ -2693,6 +2704,7 @@ foreach tuple : [ ['gnu-efi', have_gnu_efi], ['kmod'], ['xkbcommon'], + ['pcre2'], ['blkid'], ['dbus'], ['glib'], diff --git a/meson_options.txt b/meson_options.txt index f71755cfa6..eeb3f6c072 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -260,6 +260,8 @@ option('lz4', type : 'combo', choices : ['auto', 'true', 'false'], description : 'lz4 compression support') option('xkbcommon', type : 'combo', choices : ['auto', 'true', 'false'], description : 'xkbcommon keymap support') +option('pcre2', type : 'combo', choices : ['auto', 'true', 'false'], + description : 'regexp matching support using pcre2') option('glib', type : 'combo', choices : ['auto', 'true', 'false'], description : 'libglib support (for tests only)') option('dbus', type : 'combo', choices : ['auto', 'true', 'false'], diff --git a/src/basic/build.h b/src/basic/build.h index ab2a838ce9..0d078efe6f 100644 --- a/src/basic/build.h +++ b/src/basic/build.h @@ -140,6 +140,12 @@ #define _IDN_FEATURE_ "-IDN" #endif +#if HAVE_PCRE2 +#define _PCRE2_FEATURE_ "+PCRE2" +#else +#define _PCRE2_FEATURE_ "-PCRE2" +#endif + #define _CGROUP_HIEARCHY_ "default-hierarchy=" DEFAULT_HIERARCHY_NAME #define SYSTEMD_FEATURES \ @@ -163,4 +169,5 @@ _KMOD_FEATURE_ " " \ _IDN2_FEATURE_ " " \ _IDN_FEATURE_ " " \ + _PCRE2_FEATURE_ " " \ _CGROUP_HIEARCHY_ From 6becf48ca3e03cd16d58942b7c8a99f7e49275c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 12 Jan 2018 07:55:45 +0100 Subject: [PATCH 2/4] journalctl: regexp matching --- man/journalctl.xml | 11 ++++ meson.build | 3 +- src/journal/journalctl.c | 108 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/man/journalctl.xml b/man/journalctl.xml index 257ff5a816..91cd4714d5 100644 --- a/man/journalctl.xml +++ b/man/journalctl.xml @@ -578,6 +578,17 @@ priorities. + + + + + Filter output to entries where the MESSAGE= + field matches the specified regular expression. PERL-compatible regular expressions + are used, see + pcre2pattern3 + for a detailed description of the syntax. + + diff --git a/meson.build b/meson.build index 686ec6ab84..1ecc6b088b 100644 --- a/meson.build +++ b/meson.build @@ -1430,7 +1430,8 @@ exe = executable('journalctl', dependencies : [threads, libqrencode, libxz, - liblz4], + liblz4, + libpcre2], install_rpath : rootlibexecdir, install : true, install_dir : rootbindir) diff --git a/src/journal/journalctl.c b/src/journal/journalctl.c index 7078f11b0d..44c7c7aec7 100644 --- a/src/journal/journalctl.c +++ b/src/journal/journalctl.c @@ -34,6 +34,11 @@ #include #include +#if HAVE_PCRE2 +# define PCRE2_CODE_UNIT_WIDTH 8 +# include +#endif + #include "sd-bus.h" #include "sd-journal.h" @@ -76,6 +81,33 @@ #define DEFAULT_FSS_INTERVAL_USEC (15*USEC_PER_MINUTE) +#if HAVE_PCRE2 +DEFINE_TRIVIAL_CLEANUP_FUNC(pcre2_match_data*, pcre2_match_data_free); + +static int pattern_compile(const char *pattern, unsigned flags, pcre2_code **out) { + int errorcode, r; + PCRE2_SIZE erroroffset; + pcre2_code *p; + + p = pcre2_compile((PCRE2_SPTR8) pattern, + PCRE2_ZERO_TERMINATED, flags, &errorcode, &erroroffset, NULL); + if (!p) { + unsigned char buf[LINE_MAX]; + + r = pcre2_get_error_message(errorcode, buf, sizeof buf); + + log_error("Bad pattern \"%s\": %s", + pattern, + r < 0 ? "unknown error" : (char*) buf); + return -EINVAL; + } + + *out = p; + return 0; +} + +#endif + enum { /* Special values for arg_lines */ ARG_LINES_DEFAULT = -2, @@ -126,6 +158,10 @@ static uint64_t arg_vacuum_n_files = 0; static usec_t arg_vacuum_time = 0; static char **arg_output_fields = NULL; +#if HAVE_PCRE2 +static pcre2_code *arg_pattern = NULL; +#endif + static enum { ACTION_SHOW, ACTION_NEW_ID128, @@ -295,6 +331,7 @@ static void help(void) { " --user-unit=UNIT Show logs from the specified user unit\n" " -t --identifier=STRING Show entries with the specified syslog identifier\n" " -p --priority=RANGE Show entries with the specified priority\n" + " -g --grep=PATTERN Show entries with MESSSAGE matching PATTERN\n" " -e --pager-end Immediately jump to the end in the pager\n" " -f --follow Follow the journal\n" " -n --lines[=INTEGER] Number of journal entries to show\n" @@ -411,6 +448,7 @@ static int parse_argv(int argc, char *argv[]) { { "header", no_argument, NULL, ARG_HEADER }, { "identifier", required_argument, NULL, 't' }, { "priority", required_argument, NULL, 'p' }, + { "grep", required_argument, NULL, 'g' }, { "setup-keys", no_argument, NULL, ARG_SETUP_KEYS }, { "interval", required_argument, NULL, ARG_INTERVAL }, { "verify", no_argument, NULL, ARG_VERIFY }, @@ -762,6 +800,24 @@ static int parse_argv(int argc, char *argv[]) { break; } +#if HAVE_PCRE2 + case 'g': { + if (arg_pattern) { + pcre2_code_free(arg_pattern); + arg_pattern = NULL; + } + r = pattern_compile(optarg, 0, &arg_pattern); + if (r < 0) + return r; + + break; + } + +#else + case 'g': + return log_error("Compiled without pattern matching support"); +#endif + case 'S': r = parse_timestamp(optarg, &arg_since); if (r < 0) { @@ -2468,6 +2524,53 @@ int main(int argc, char *argv[]) { } } +#if HAVE_PCRE2 + if (arg_pattern) { + _cleanup_(pcre2_match_data_freep) pcre2_match_data *md = NULL; + const void *message; + size_t len; + + md = pcre2_match_data_create(1, NULL); + if (!md) + return log_oom(); + + r = sd_journal_get_data(j, "MESSAGE", &message, &len); + if (r < 0) { + if (r == -ENOENT) { + need_seek = true; + continue; + } + + log_error_errno(r, "Failed to get MESSAGE field: %m"); + goto finish; + } + + assert_se(message = startswith(message, "MESSAGE=")); + + r = pcre2_match(arg_pattern, + message, + len - strlen("MESSAGE="), + 0, /* start at offset 0 in the subject */ + 0, /* default options */ + md, + NULL); + if (r == PCRE2_ERROR_NOMATCH) { + need_seek = true; + continue; + } + if (r < 0) { + unsigned char buf[LINE_MAX]; + int r2; + + r2 = pcre2_get_error_message(r, buf, sizeof buf); + log_error("Pattern matching failed: %s", + r2 < 0 ? "unknown error" : (char*) buf); + r = -EINVAL; + goto finish; + } + } +#endif + flags = arg_all * OUTPUT_SHOW_ALL | arg_full * OUTPUT_FULL_WIDTH | @@ -2527,5 +2630,10 @@ finish: free(arg_root); free(arg_verify_key); +#if HAVE_PCRE2 + if (arg_pattern) + pcre2_code_free(arg_pattern); +#endif + return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS; } From 61c5f8a1f06832a2bcad9ab75edfa4f652df2347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 12 Jan 2018 14:31:49 +0100 Subject: [PATCH 3/4] journalctl: make matching optionally case sensitive Case sensitive or case insensitive matching can be requested using --case-sensitive[=yes|no]. Unless specified, matching is case sensitive if the pattern contains any uppercase letters, and case insensitive otherwise. This matches what forward-search does in emacs, and recently also --ignore-case in less. This works surprisingly well, because usually when one is wants to do case-sensitive matching, the pattern is usually camel-cased. In the less frequent case when case-sensitive matching is required with an all-lowercase pattern, --case-sensitive can be used to override the automatic logic. --- man/journalctl.xml | 14 ++- src/journal/journalctl.c | 191 ++++++++++++++++++++++++--------------- src/test/meson.build | 1 - 3 files changed, 131 insertions(+), 75 deletions(-) diff --git a/man/journalctl.xml b/man/journalctl.xml index 91cd4714d5..37fb0d67fd 100644 --- a/man/journalctl.xml +++ b/man/journalctl.xml @@ -586,7 +586,19 @@ field matches the specified regular expression. PERL-compatible regular expressions are used, see pcre2pattern3 - for a detailed description of the syntax. + for a detailed description of the syntax. + + If the pattern is all lowercase, matching is case insensitive. + Otherwise, matching is case sensitive. This can be overridden with the + option, see below. + + + + + + + Make pattern matching case sensitive or case insenstive. + diff --git a/src/journal/journalctl.c b/src/journal/journalctl.c index 44c7c7aec7..757e9df5ba 100644 --- a/src/journal/journalctl.c +++ b/src/journal/journalctl.c @@ -83,6 +83,7 @@ #if HAVE_PCRE2 DEFINE_TRIVIAL_CLEANUP_FUNC(pcre2_match_data*, pcre2_match_data_free); +DEFINE_TRIVIAL_CLEANUP_FUNC(pcre2_code*, pcre2_code_free); static int pattern_compile(const char *pattern, unsigned flags, pcre2_code **out) { int errorcode, r; @@ -159,7 +160,9 @@ static usec_t arg_vacuum_time = 0; static char **arg_output_fields = NULL; #if HAVE_PCRE2 -static pcre2_code *arg_pattern = NULL; +static const char *arg_pattern = NULL; +static pcre2_code *arg_compiled_pattern = NULL; +static int arg_case_sensitive = -1; /* -1 means be smart */ #endif static enum { @@ -316,68 +319,69 @@ static void help(void) { printf("%s [OPTIONS...] [MATCHES...]\n\n" "Query the journal.\n\n" "Options:\n" - " --system Show the system journal\n" - " --user Show the user journal for the current user\n" - " -M --machine=CONTAINER Operate on local container\n" - " -S --since=DATE Show entries not older than the specified date\n" - " -U --until=DATE Show entries not newer than the specified date\n" - " -c --cursor=CURSOR Show entries starting at the specified cursor\n" - " --after-cursor=CURSOR Show entries after the specified cursor\n" - " --show-cursor Print the cursor after all the entries\n" - " -b --boot[=ID] Show current boot or the specified boot\n" - " --list-boots Show terse information about recorded boots\n" - " -k --dmesg Show kernel message log from the current boot\n" - " -u --unit=UNIT Show logs from the specified unit\n" - " --user-unit=UNIT Show logs from the specified user unit\n" - " -t --identifier=STRING Show entries with the specified syslog identifier\n" - " -p --priority=RANGE Show entries with the specified priority\n" - " -g --grep=PATTERN Show entries with MESSSAGE matching PATTERN\n" - " -e --pager-end Immediately jump to the end in the pager\n" - " -f --follow Follow the journal\n" - " -n --lines[=INTEGER] Number of journal entries to show\n" - " --no-tail Show all lines, even in follow mode\n" - " -r --reverse Show the newest entries first\n" - " -o --output=STRING Change journal output mode (short, short-precise,\n" - " short-iso, short-iso-precise, short-full,\n" - " short-monotonic, short-unix, verbose, export,\n" - " json, json-pretty, json-sse, cat)\n" - " --output-fields=LIST Select fields to print in verbose/export/json modes\n" - " --utc Express time in Coordinated Universal Time (UTC)\n" - " -x --catalog Add message explanations where available\n" - " --no-full Ellipsize fields\n" - " -a --all Show all fields, including long and unprintable\n" - " -q --quiet Do not show info messages and privilege warning\n" - " --no-pager Do not pipe output into a pager\n" - " --no-hostname Suppress output of hostname field\n" - " -m --merge Show entries from all available journals\n" - " -D --directory=PATH Show journal files from directory\n" - " --file=PATH Show journal file\n" - " --root=ROOT Operate on files below a root directory\n" + " --system Show the system journal\n" + " --user Show the user journal for the current user\n" + " -M --machine=CONTAINER Operate on local container\n" + " -S --since=DATE Show entries not older than the specified date\n" + " -U --until=DATE Show entries not newer than the specified date\n" + " -c --cursor=CURSOR Show entries starting at the specified cursor\n" + " --after-cursor=CURSOR Show entries after the specified cursor\n" + " --show-cursor Print the cursor after all the entries\n" + " -b --boot[=ID] Show current boot or the specified boot\n" + " --list-boots Show terse information about recorded boots\n" + " -k --dmesg Show kernel message log from the current boot\n" + " -u --unit=UNIT Show logs from the specified unit\n" + " --user-unit=UNIT Show logs from the specified user unit\n" + " -t --identifier=STRING Show entries with the specified syslog identifier\n" + " -p --priority=RANGE Show entries with the specified priority\n" + " -g --grep=PATTERN Show entries with MESSSAGE matching PATTERN\n" + " --case-sensitive[=BOOL] Force case sensitive or insenstive matching\n" + " -e --pager-end Immediately jump to the end in the pager\n" + " -f --follow Follow the journal\n" + " -n --lines[=INTEGER] Number of journal entries to show\n" + " --no-tail Show all lines, even in follow mode\n" + " -r --reverse Show the newest entries first\n" + " -o --output=STRING Change journal output mode (short, short-precise,\n" + " short-iso, short-iso-precise, short-full,\n" + " short-monotonic, short-unix, verbose, export,\n" + " json, json-pretty, json-sse, cat)\n" + " --output-fields=LIST Select fields to print in verbose/export/json modes\n" + " --utc Express time in Coordinated Universal Time (UTC)\n" + " -x --catalog Add message explanations where available\n" + " --no-full Ellipsize fields\n" + " -a --all Show all fields, including long and unprintable\n" + " -q --quiet Do not show info messages and privilege warning\n" + " --no-pager Do not pipe output into a pager\n" + " --no-hostname Suppress output of hostname field\n" + " -m --merge Show entries from all available journals\n" + " -D --directory=PATH Show journal files from directory\n" + " --file=PATH Show journal file\n" + " --root=ROOT Operate on files below a root directory\n" #if HAVE_GCRYPT - " --interval=TIME Time interval for changing the FSS sealing key\n" - " --verify-key=KEY Specify FSS verification key\n" - " --force Override of the FSS key pair with --setup-keys\n" + " --interval=TIME Time interval for changing the FSS sealing key\n" + " --verify-key=KEY Specify FSS verification key\n" + " --force Override of the FSS key pair with --setup-keys\n" #endif "\nCommands:\n" - " -h --help Show this help text\n" - " --version Show package version\n" - " -N --fields List all field names currently used\n" - " -F --field=FIELD List all values that a specified field takes\n" - " --disk-usage Show total disk usage of all journal files\n" - " --vacuum-size=BYTES Reduce disk usage below specified size\n" - " --vacuum-files=INT Leave only the specified number of journal files\n" - " --vacuum-time=TIME Remove journal files older than specified time\n" - " --verify Verify journal file consistency\n" - " --sync Synchronize unwritten journal messages to disk\n" - " --flush Flush all journal data from /run into /var\n" - " --rotate Request immediate rotation of the journal files\n" - " --header Show journal header information\n" - " --list-catalog Show all message IDs in the catalog\n" - " --dump-catalog Show entries in the message catalog\n" - " --update-catalog Update the message catalog database\n" - " --new-id128 Generate a new 128-bit ID\n" + " -h --help Show this help text\n" + " --version Show package version\n" + " -N --fields List all field names currently used\n" + " -F --field=FIELD List all values that a specified field takes\n" + " --disk-usage Show total disk usage of all journal files\n" + " --vacuum-size=BYTES Reduce disk usage below specified size\n" + " --vacuum-files=INT Leave only the specified number of journal files\n" + " --vacuum-time=TIME Remove journal files older than specified time\n" + " --verify Verify journal file consistency\n" + " --sync Synchronize unwritten journal messages to disk\n" + " --flush Flush all journal data from /run into /var\n" + " --rotate Request immediate rotation of the journal files\n" + " --header Show journal header information\n" + " --list-catalog Show all message IDs in the catalog\n" + " --dump-catalog Show entries in the message catalog\n" + " --update-catalog Update the message catalog database\n" + " --new-id128 Generate a new 128-bit ID\n" #if HAVE_GCRYPT - " --setup-keys Generate a new FSS key pair\n" + " --setup-keys Generate a new FSS key pair\n" #endif , program_invocation_short_name); } @@ -409,6 +413,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_DUMP_CATALOG, ARG_UPDATE_CATALOG, ARG_FORCE, + ARG_CASE_SENSITIVE, ARG_UTC, ARG_SYNC, ARG_FLUSH, @@ -449,6 +454,7 @@ static int parse_argv(int argc, char *argv[]) { { "identifier", required_argument, NULL, 't' }, { "priority", required_argument, NULL, 'p' }, { "grep", required_argument, NULL, 'g' }, + { "case-sensitive", optional_argument, NULL, ARG_CASE_SENSITIVE }, { "setup-keys", no_argument, NULL, ARG_SETUP_KEYS }, { "interval", required_argument, NULL, ARG_INTERVAL }, { "verify", no_argument, NULL, ARG_VERIFY }, @@ -801,20 +807,23 @@ static int parse_argv(int argc, char *argv[]) { } #if HAVE_PCRE2 - case 'g': { - if (arg_pattern) { - pcre2_code_free(arg_pattern); - arg_pattern = NULL; - } - r = pattern_compile(optarg, 0, &arg_pattern); - if (r < 0) - return r; + case 'g': + arg_pattern = optarg; + break; + + case ARG_CASE_SENSITIVE: + if (optarg) { + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Bad --case-sensitive= argument \"%s\": %m", optarg); + arg_case_sensitive = r; + } else + arg_case_sensitive = true; break; - } - #else case 'g': + case ARG_CASE_SENSITIVE: return log_error("Compiled without pattern matching support"); #endif @@ -973,6 +982,42 @@ static int parse_argv(int argc, char *argv[]) { arg_system_units = strv_free(arg_system_units); } + +#if HAVE_PCRE2 + if (arg_pattern) { + unsigned flags; + + if (arg_case_sensitive >= 0) + flags = !arg_case_sensitive * PCRE2_CASELESS; + else { + _cleanup_(pcre2_match_data_freep) pcre2_match_data *md = NULL; + bool has_case; + _cleanup_(pcre2_code_freep) pcre2_code *cs = NULL; + + md = pcre2_match_data_create(1, NULL); + if (!md) + return log_oom(); + + r = pattern_compile("[[:upper:]]", 0, &cs); + if (r < 0) + return r; + + r = pcre2_match(cs, (PCRE2_SPTR8) arg_pattern, PCRE2_ZERO_TERMINATED, 0, 0, md, NULL); + has_case = r >= 0; + + flags = !has_case * PCRE2_CASELESS; + } + + log_debug("Doing case %s matching based on %s", + flags & PCRE2_CASELESS ? "insensitive" : "sensitive", + arg_case_sensitive >= 0 ? "request" : "pattern casing"); + + r = pattern_compile(arg_pattern, flags, &arg_compiled_pattern); + if (r < 0) + return r; + } +#endif + return 1; } @@ -2525,7 +2570,7 @@ int main(int argc, char *argv[]) { } #if HAVE_PCRE2 - if (arg_pattern) { + if (arg_compiled_pattern) { _cleanup_(pcre2_match_data_freep) pcre2_match_data *md = NULL; const void *message; size_t len; @@ -2547,7 +2592,7 @@ int main(int argc, char *argv[]) { assert_se(message = startswith(message, "MESSAGE=")); - r = pcre2_match(arg_pattern, + r = pcre2_match(arg_compiled_pattern, message, len - strlen("MESSAGE="), 0, /* start at offset 0 in the subject */ @@ -2631,8 +2676,8 @@ finish: free(arg_verify_key); #if HAVE_PCRE2 - if (arg_pattern) - pcre2_code_free(arg_pattern); + if (arg_compiled_pattern) + pcre2_code_free(arg_compiled_pattern); #endif return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS; diff --git a/src/test/meson.build b/src/test/meson.build index 18e957ddc8..f51f06dda0 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -500,7 +500,6 @@ tests += [ [], '', 'manual'], - [['src/test/test-cgroup-mask.c', 'src/test/test-helper.c'], [libcore, From b4766d5f154c6645b73a81b706c1c43f6641a78a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Sat, 27 Jan 2018 13:00:09 +0100 Subject: [PATCH 4/4] journalctl: add highlighting for matched substring Red is used for highligting, the same as grep does. Except when the line is highlighted red already, because it has high priority, in which case plain ansi highlight is used for the matched substring. Coloring is implemented for short and cat outputs, and not for other types. I guess we could also add it for verbose output in the future. --- src/basic/string-util.c | 29 +++++++-- src/basic/string-util.h | 2 +- src/journal-remote/journal-gatewayd.c | 3 +- src/journal/journalctl.c | 9 ++- src/shared/logs-show.c | 90 ++++++++++++++++++++++----- src/shared/logs-show.h | 1 + src/test/test-strip-tab-ansi.c | 8 +-- 7 files changed, 114 insertions(+), 28 deletions(-) diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 7e2f596edc..9f2c01d864 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -30,6 +30,7 @@ #include "gunicode.h" #include "macro.h" #include "string-util.h" +#include "terminal-util.h" #include "utf8.h" #include "util.h" @@ -648,7 +649,17 @@ char *strreplace(const char *text, const char *old_string, const char *new_strin return ret; } -char *strip_tab_ansi(char **ibuf, size_t *_isz) { +static void advance_offsets(ssize_t diff, size_t offsets[2], size_t shift[2], size_t size) { + if (!offsets) + return; + + if ((size_t) diff < offsets[0]) + shift[0] += size; + if ((size_t) diff < offsets[1]) + shift[1] += size; +} + +char *strip_tab_ansi(char **ibuf, size_t *_isz, size_t highlight[2]) { const char *i, *begin = NULL; enum { STATE_OTHER, @@ -656,7 +667,7 @@ char *strip_tab_ansi(char **ibuf, size_t *_isz) { STATE_BRACKET } state = STATE_OTHER; char *obuf = NULL; - size_t osz = 0, isz; + size_t osz = 0, isz, shift[2] = {}; FILE *f; assert(ibuf); @@ -684,15 +695,18 @@ char *strip_tab_ansi(char **ibuf, size_t *_isz) { break; else if (*i == '\x1B') state = STATE_ESCAPE; - else if (*i == '\t') + else if (*i == '\t') { fputs(" ", f); - else + advance_offsets(i - *ibuf, highlight, shift, 7); + } else fputc(*i, f); + break; case STATE_ESCAPE: if (i >= *ibuf + isz) { /* EOT */ fputc('\x1B', f); + advance_offsets(i - *ibuf, highlight, shift, 1); break; } else if (*i == '[') { state = STATE_BRACKET; @@ -700,6 +714,7 @@ char *strip_tab_ansi(char **ibuf, size_t *_isz) { } else { fputc('\x1B', f); fputc(*i, f); + advance_offsets(i - *ibuf, highlight, shift, 1); state = STATE_OTHER; } @@ -711,6 +726,7 @@ char *strip_tab_ansi(char **ibuf, size_t *_isz) { (!(*i >= '0' && *i <= '9') && !IN_SET(*i, ';', 'm'))) { fputc('\x1B', f); fputc('[', f); + advance_offsets(i - *ibuf, highlight, shift, 2); state = STATE_OTHER; i = begin-1; } else if (*i == 'm') @@ -732,6 +748,11 @@ char *strip_tab_ansi(char **ibuf, size_t *_isz) { if (_isz) *_isz = osz; + if (highlight) { + highlight[0] += shift[0]; + highlight[1] += shift[1]; + } + return obuf; } diff --git a/src/basic/string-util.h b/src/basic/string-util.h index d8f97e697b..08eda4fce0 100644 --- a/src/basic/string-util.h +++ b/src/basic/string-util.h @@ -177,7 +177,7 @@ char* strshorten(char *s, size_t l); char *strreplace(const char *text, const char *old_string, const char *new_string); -char *strip_tab_ansi(char **p, size_t *l); +char *strip_tab_ansi(char **ibuf, size_t *_isz, size_t highlight[2]); char *strextend_with_separator(char **x, const char *separator, ...) _sentinel_; diff --git a/src/journal-remote/journal-gatewayd.c b/src/journal-remote/journal-gatewayd.c index 82c70cfbe3..5a437ce031 100644 --- a/src/journal-remote/journal-gatewayd.c +++ b/src/journal-remote/journal-gatewayd.c @@ -226,7 +226,8 @@ static ssize_t request_reader_entries( return MHD_CONTENT_READER_END_WITH_ERROR; } - r = output_journal(m->tmp, m->journal, m->mode, 0, OUTPUT_FULL_WIDTH, NULL, NULL); + r = output_journal(m->tmp, m->journal, m->mode, 0, OUTPUT_FULL_WIDTH, + NULL, NULL, NULL); if (r < 0) { log_error_errno(r, "Failed to serialize item: %m"); return MHD_CONTENT_READER_END_WITH_ERROR; diff --git a/src/journal/journalctl.c b/src/journal/journalctl.c index 757e9df5ba..17782688d9 100644 --- a/src/journal/journalctl.c +++ b/src/journal/journalctl.c @@ -2516,6 +2516,7 @@ int main(int argc, char *argv[]) { for (;;) { while (arg_lines < 0 || n_shown < arg_lines || (arg_follow && !first_line)) { int flags; + size_t highlight[2] = {}; if (need_seek) { if (!arg_reverse) @@ -2574,6 +2575,7 @@ int main(int argc, char *argv[]) { _cleanup_(pcre2_match_data_freep) pcre2_match_data *md = NULL; const void *message; size_t len; + PCRE2_SIZE *ovec; md = pcre2_match_data_create(1, NULL); if (!md) @@ -2613,6 +2615,10 @@ int main(int argc, char *argv[]) { r = -EINVAL; goto finish; } + + ovec = pcre2_get_ovector_pointer(md); + highlight[0] = ovec[0]; + highlight[1] = ovec[1]; } #endif @@ -2624,7 +2630,8 @@ int main(int argc, char *argv[]) { arg_utc * OUTPUT_UTC | arg_no_hostname * OUTPUT_NO_HOSTNAME; - r = output_journal(stdout, j, arg_output, 0, flags, arg_output_fields, &ellipsized); + r = output_journal(stdout, j, arg_output, 0, flags, + arg_output_fields, highlight, &ellipsized); need_seek = true; if (r == -EADDRNOTAVAIL) break; diff --git a/src/shared/logs-show.c b/src/shared/logs-show.c index a861be7b32..3d0be7ab00 100644 --- a/src/shared/logs-show.c +++ b/src/shared/logs-show.c @@ -166,8 +166,17 @@ static bool shall_print(const char *p, size_t l, OutputFlags flags) { return true; } -static bool print_multiline(FILE *f, unsigned prefix, unsigned n_columns, OutputFlags flags, int priority, const char* message, size_t message_len) { - const char *color_on = "", *color_off = ""; +static bool print_multiline( + FILE *f, + unsigned prefix, + unsigned n_columns, + OutputFlags flags, + int priority, + const char* message, + size_t message_len, + size_t highlight[2]) { + + const char *color_on = "", *color_off = "", *highlight_on = ""; const char *pos, *end; bool ellipsized = false; int line = 0; @@ -176,9 +185,11 @@ static bool print_multiline(FILE *f, unsigned prefix, unsigned n_columns, Output if (priority <= LOG_ERR) { color_on = ANSI_HIGHLIGHT_RED; color_off = ANSI_NORMAL; + highlight_on = ANSI_HIGHLIGHT; } else if (priority <= LOG_NOTICE) { color_on = ANSI_HIGHLIGHT; color_off = ANSI_NORMAL; + highlight_on = ANSI_HIGHLIGHT_RED; } } @@ -209,9 +220,28 @@ static bool print_multiline(FILE *f, unsigned prefix, unsigned n_columns, Output if (flags & (OUTPUT_FULL_WIDTH | OUTPUT_SHOW_ALL) || (prefix + len + 1 < n_columns && !tail_line)) { - fprintf(f, "%*s%s%.*s%s\n", - continuation * prefix, "", - color_on, len, pos, color_off); + if (highlight && + (size_t) (pos - message) <= highlight[0] && + highlight[0] < (size_t) len) { + + fprintf(f, "%*s%s%.*s", + continuation * prefix, "", + color_on, (int) highlight[0], pos); + fprintf(f, "%s%.*s", + highlight_on, + (int) (MIN((size_t) len, highlight[1]) - highlight[0]), + pos + highlight[0]); + if ((size_t) len > highlight[1]) + fprintf(f, "%s%.*s", + color_on, + (int) (len - highlight[1]), + pos + highlight[1]); + fprintf(f, "%s\n", color_off); + + } else + fprintf(f, "%*s%s%.*s%s\n", + continuation * prefix, "", + color_on, len, pos, color_off); continue; } @@ -369,7 +399,8 @@ static int output_short( OutputMode mode, unsigned n_columns, OutputFlags flags, - Set *output_fields) { + Set *output_fields, + size_t highlight[2]) { int r; const void *data; @@ -390,6 +421,7 @@ static int output_short( PARSE_FIELD_VEC_ENTRY("_SOURCE_REALTIME_TIMESTAMP=", &realtime, &realtime_len), PARSE_FIELD_VEC_ENTRY("_SOURCE_MONOTONIC_TIMESTAMP=", &monotonic, &monotonic_len), }; + size_t highlight_shifted[] = {highlight ? highlight[0] : 0, highlight ? highlight[1] : 0}; assert(f); assert(j); @@ -421,7 +453,7 @@ static int output_short( } if (!(flags & OUTPUT_SHOW_ALL)) - strip_tab_ansi(&message, &message_len); + strip_tab_ansi(&message, &message_len, highlight_shifted); if (priority_len == 1 && *priority >= '0' && *priority <= '7') p = *priority - '0'; @@ -468,7 +500,9 @@ static int output_short( } else { fputs(": ", f); ellipsized |= - print_multiline(f, n + 2, n_columns, flags, p, message, message_len); + print_multiline(f, n + 2, n_columns, flags, p, + message, message_len, + highlight_shifted); } if (flags & OUTPUT_CATALOG) @@ -483,7 +517,8 @@ static int output_verbose( OutputMode mode, unsigned n_columns, OutputFlags flags, - Set *output_fields) { + Set *output_fields, + size_t highlight[2]) { const void *data; size_t length; @@ -561,7 +596,7 @@ static int output_verbose( (((length < PRINT_CHAR_THRESHOLD) || flags & OUTPUT_FULL_WIDTH) && utf8_is_printable(data, length))) { fprintf(f, " %s%.*s=", on, fieldlen, (const char*)data); - print_multiline(f, 4 + fieldlen + 1, 0, OUTPUT_FULL_WIDTH, 0, c + 1, length - fieldlen - 1); + print_multiline(f, 4 + fieldlen + 1, 0, OUTPUT_FULL_WIDTH, 0, c + 1, length - fieldlen - 1, NULL); fputs(off, f); } else { char bytes[FORMAT_BYTES_MAX]; @@ -590,7 +625,8 @@ static int output_export( OutputMode mode, unsigned n_columns, OutputFlags flags, - Set *output_fields) { + Set *output_fields, + size_t highlight[2]) { sd_id128_t boot_id; char sid[33]; @@ -728,7 +764,8 @@ static int output_json( OutputMode mode, unsigned n_columns, OutputFlags flags, - Set *output_fields) { + Set *output_fields, + size_t highlight[2]) { uint64_t realtime, monotonic; _cleanup_free_ char *cursor = NULL; @@ -952,15 +989,22 @@ static int output_cat( OutputMode mode, unsigned n_columns, OutputFlags flags, - Set *output_fields) { + Set *output_fields, + size_t highlight[2]) { const void *data; size_t l; int r; + const char *highlight_on = "", *highlight_off = ""; assert(j); assert(f); + if (flags & OUTPUT_COLOR) { + highlight_on = ANSI_HIGHLIGHT_RED; + highlight_off = ANSI_NORMAL; + } + sd_journal_set_data_threshold(j, 0); r = sd_journal_get_data(j, "MESSAGE", &data, &l); @@ -974,7 +1018,17 @@ static int output_cat( assert(l >= 8); - fwrite((const char*) data + 8, 1, l - 8, f); + if (highlight && (flags & OUTPUT_COLOR)) { + assert(highlight[0] <= highlight[1]); + assert(highlight[1] <= l - 8); + + fwrite((const char*) data + 8, 1, highlight[0], f); + fwrite(highlight_on, 1, strlen(highlight_on), f); + fwrite((const char*) data + 8 + highlight[0], 1, highlight[1] - highlight[0], f); + fwrite(highlight_off, 1, strlen(highlight_off), f); + fwrite((const char*) data + 8 + highlight[1], 1, l - 8 - highlight[1], f); + } else + fwrite((const char*) data + 8, 1, l - 8, f); fputc('\n', f); return 0; @@ -986,7 +1040,8 @@ static int (*output_funcs[_OUTPUT_MODE_MAX])( OutputMode mode, unsigned n_columns, OutputFlags flags, - Set *output_fields) = { + Set *output_fields, + size_t highlight[2]) = { [OUTPUT_SHORT] = output_short, [OUTPUT_SHORT_ISO] = output_short, @@ -1010,6 +1065,7 @@ int output_journal( unsigned n_columns, OutputFlags flags, char **output_fields, + size_t highlight[2], bool *ellipsized) { int ret; @@ -1030,7 +1086,7 @@ int output_journal( return ret; } - ret = output_funcs[mode](f, j, mode, n_columns, flags, fields); + ret = output_funcs[mode](f, j, mode, n_columns, flags, fields, highlight); if (ellipsized && ret > 0) *ellipsized = true; @@ -1112,7 +1168,7 @@ static int show_journal(FILE *f, line++; maybe_print_begin_newline(f, &flags); - r = output_journal(f, j, mode, n_columns, flags, NULL, ellipsized); + r = output_journal(f, j, mode, n_columns, flags, NULL, NULL, ellipsized); if (r < 0) return r; } diff --git a/src/shared/logs-show.h b/src/shared/logs-show.h index eaa69b6e90..c876dcc46a 100644 --- a/src/shared/logs-show.h +++ b/src/shared/logs-show.h @@ -39,6 +39,7 @@ int output_journal( unsigned n_columns, OutputFlags flags, char **output_fields, + size_t highlight[2], bool *ellipsized); int add_match_this_boot(sd_journal *j, const char *machine); diff --git a/src/test/test-strip-tab-ansi.c b/src/test/test-strip-tab-ansi.c index aabb7c40e7..838a6e4db6 100644 --- a/src/test/test-strip-tab-ansi.c +++ b/src/test/test-strip-tab-ansi.c @@ -28,24 +28,24 @@ int main(int argc, char *argv[]) { char *p; assert_se(p = strdup("\tFoobar\tbar\twaldo\t")); - assert_se(strip_tab_ansi(&p, NULL)); + assert_se(strip_tab_ansi(&p, NULL, NULL)); fprintf(stdout, "<%s>\n", p); assert_se(streq(p, " Foobar bar waldo ")); free(p); assert_se(p = strdup(ANSI_HIGHLIGHT "Hello" ANSI_NORMAL ANSI_HIGHLIGHT_RED " world!" ANSI_NORMAL)); - assert_se(strip_tab_ansi(&p, NULL)); + assert_se(strip_tab_ansi(&p, NULL, NULL)); fprintf(stdout, "<%s>\n", p); assert_se(streq(p, "Hello world!")); free(p); assert_se(p = strdup("\x1B[\x1B[\t\x1B[" ANSI_HIGHLIGHT "\x1B[" "Hello" ANSI_NORMAL ANSI_HIGHLIGHT_RED " world!" ANSI_NORMAL)); - assert_se(strip_tab_ansi(&p, NULL)); + assert_se(strip_tab_ansi(&p, NULL, NULL)); assert_se(streq(p, "\x1B[\x1B[ \x1B[\x1B[Hello world!")); free(p); assert_se(p = strdup("\x1B[waldo")); - assert_se(strip_tab_ansi(&p, NULL)); + assert_se(strip_tab_ansi(&p, NULL, NULL)); assert_se(streq(p, "\x1B[waldo")); free(p);