ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
grep.h File Reference

Go to the source code of this file.

Data Structures

struct  grep_parse_result_t
 Parsed pattern result from /pattern/flags syntax. More...

Macros

#define LOG_GREP_PATTERN_BUFFER_SIZE   4096

Functions

grep_parse_result_t grep_parse_pattern (const char *input)
 Parse pattern in /pattern/flags format or plain regex format.
asciichat_error_t grep_init (const char *pattern)
 Initialize log filtering with PCRE2 regex pattern.
bool grep_should_output (const char *log_line, size_t *match_start, size_t *match_len)
 Check if log line should be output (matches filter).
const char * grep_highlight (const char *log_line, size_t match_start, size_t match_len)
 Highlight matched portion with background color.
const char * grep_highlight_colored (const char *colored_text, const char *plain_text, size_t match_start, size_t match_len)
 Highlight matches in colored text while preserving original colors.
char * grep_highlight_colored_copy (const char *colored_text, const char *plain_text, size_t match_start, size_t match_len)
 Highlight matches in colored text, returning a new allocated copy.
void grep_destroy (void)
 Clean up filter resources.
asciichat_error_t grep_save_patterns (void)
 Save current filter patterns for later restoration.
asciichat_error_t grep_restore_patterns (void)
 Restore previously saved filter patterns.
void grep_clear_patterns (void)
 Clear all filter patterns.
int grep_get_pattern_count (void)
 Get number of active filter patterns.
const char * grep_get_last_pattern (void)
 Get the last (most recent) filter pattern string.

Macro Definition Documentation

◆ LOG_GREP_PATTERN_BUFFER_SIZE

#define LOG_GREP_PATTERN_BUFFER_SIZE   4096

Pattern storage capacity in bytes, including NUL.

Definition at line 9 of file grep.h.

Referenced by grep_highlight_colored().

Function Documentation

◆ grep_clear_patterns()

void grep_clear_patterns ( void )

Clear all filter patterns.

Temporarily disables filtering by setting pattern count to 0. Patterns remain allocated and can be restored later. Used by interactive grep when user types first character.

Definition at line 1124 of file grep.c.

1124 {
1125 for (int i = 0; i < g_filter_state.pattern_count; i++)
1126 grep_free_pattern(&g_filter_state.patterns[i]);
1127 g_filter_state.pattern_count = 0;
1128 g_filter_state.enabled = false;
1129}

◆ grep_destroy()

void grep_destroy ( void )

Clean up filter resources.

Frees compiled regex. Thread-local match_data is cleaned up via pthread_key destructor.

Definition at line 1005 of file grep.c.

1005 {
1006 // Free all patterns
1007 for (int i = 0; i < g_filter_state.pattern_count; i++) {
1008 grep_pattern_t *pat = &g_filter_state.patterns[i];
1009 // Free the copied original string (need temp variable due to const qualifier)
1010 char *original_to_free = (char *)pat->original;
1011 SAFE_FREE(original_to_free);
1012 SAFE_FREE(pat->parsed_pattern);
1013 if (pat->compiled)
1014 pcre2_code_free(pat->compiled);
1015 pat->compiled = NULL;
1016 }
1017 SAFE_FREE(g_filter_state.patterns);
1018 g_filter_state.patterns = NULL; // Make idempotent
1019 for (int i = 0; i < g_filter_state.saved_pattern_count; i++) {
1020 grep_pattern_t *saved = &g_filter_state.saved_patterns[i];
1021 char *original = (char *)saved->original;
1022 SAFE_FREE(original);
1023 SAFE_FREE(saved->parsed_pattern);
1024 if (saved->compiled)
1025 pcre2_code_free(saved->compiled);
1026 }
1027 SAFE_FREE(g_filter_state.saved_patterns);
1028 g_filter_state.saved_pattern_count = 0;
1029 g_filter_state.saved_pattern_capacity = 0;
1030
1031 // Free context buffer
1032 if (g_filter_state.line_buffer) {
1033 for (int i = 0; i < g_filter_state.buffer_size; i++) {
1034 SAFE_FREE(g_filter_state.line_buffer[i]);
1035 }
1036 SAFE_FREE(g_filter_state.line_buffer);
1037 g_filter_state.line_buffer = NULL; // Make idempotent
1038 }
1039
1040 g_filter_state.pattern_count = 0;
1041 g_filter_state.pattern_capacity = 0;
1042 g_filter_state.buffer_size = 0;
1043 g_filter_state.buffer_pos = 0;
1044 g_filter_state.lines_after_match = 0;
1045 g_filter_state.max_context_after = 0;
1046 g_filter_state.enabled = false;
1047
1048 // Note: Thread-local match_data is cleaned up via pthread_key destructor
1049}
#define SAFE_FREE(ptr)
Definition common.h:377
grep pattern keeps the values that move through the subsystem: original, parsed_pattern,...
Definition grep.c:43
const char * original
Original pattern string (for display).
Definition grep.c:44

References grep_pattern_t::compiled, grep_pattern_t::original, grep_pattern_t::parsed_pattern, and SAFE_FREE.

Referenced by log_destroy().

◆ grep_get_last_pattern()

const char * grep_get_last_pattern ( void )

Get the last (most recent) filter pattern string.

Returns the original pattern string (with /pattern/flags syntax if provided) of the last –grep argument. Used by interactive grep to populate its input buffer when starting with CLI –grep patterns.

Returns
Pattern string of last CLI –grep, or NULL if no patterns

Definition at line 1135 of file grep.c.

1135 {
1136 if (g_filter_state.pattern_count == 0) {
1137 return NULL;
1138 }
1139
1140 // Return the original pattern string of the last pattern
1141 return g_filter_state.patterns[g_filter_state.pattern_count - 1].original;
1142}

Referenced by log_search_enter_mode(), log_search_init(), ui_status_display(), and ui_status_display_interactive().

◆ grep_get_pattern_count()

int grep_get_pattern_count ( void )

Get number of active filter patterns.

Returns
Number of currently active patterns

Definition at line 1131 of file grep.c.

1131 {
1132 return g_filter_state.pattern_count;
1133}

◆ grep_highlight()

const char * grep_highlight ( const char * log_line,
size_t match_start,
size_t match_len )

Highlight matched portion with background color.

Inserts ANSI escape codes for yellow background (255, 200, 0) with black foreground (0, 0, 0) around the matched text.

Parameters
log_lineOriginal log line
match_startStart offset of match
match_lenLength of match
Returns
Highlighted string (pointer to static thread-local buffer)

Definition at line 999 of file grep.c.

999 {
1000 if (!log_line || match_start > strlen(log_line) || match_len > strlen(log_line) - match_start)
1001 return log_line;
1002 return grep_highlight_colored(log_line, log_line, match_start, match_len);
1003}
const char * grep_highlight_colored(const char *colored_text, const char *plain_text, size_t match_start, size_t match_len)
Highlight matches in colored text while preserving original colors.
Definition grep.c:670

References grep_highlight_colored().

◆ grep_highlight_colored()

const char * grep_highlight_colored ( const char * colored_text,
const char * plain_text,
size_t match_start,
size_t match_len )

Highlight matches in colored text while preserving original colors.

Parameters
colored_textOriginal text with ANSI color codes
plain_textStripped plain text (for match positions)
match_startMatch start position in plain text
match_lenMatch length in plain text
Returns
Highlighted text with yellow background added to matches (pointer to static thread-local buffer)

Definition at line 670 of file grep.c.

671 {
672 static __thread char highlight_buffer[LOG_TERMINAL_BUFFER_SIZE];
673
674 if (!colored_text || !plain_text || match_len == 0) {
675 return colored_text;
676 }
677
678 // When interactive grep is active, check if it has global flag
679 bool is_log_search = log_search_is_active();
680 bool should_use_global_pattern = false;
681 grep_pattern_t *global_pat = NULL;
682
683 if (!is_log_search) {
684 // If any pattern has global flag, highlight ALL matches for that pattern
685 // Find first pattern with global flag
686 for (int i = 0; i < g_filter_state.pattern_count; i++) {
687 if (g_filter_state.patterns[i].global_flag) {
688 global_pat = &g_filter_state.patterns[i];
689 should_use_global_pattern = true;
690 break;
691 }
692 }
693 } else if (log_search_get_global_highlight()) {
694 // Interactive grep is active and has /g flag - do global matching on the provided pattern
695 // We'll do all-match highlighting using the match position to extract the pattern
696 // and re-match for all occurrences
697 should_use_global_pattern = true;
698 // global_pat stays NULL, but we set the flag to enable global matching below
699 }
700
701 if (should_use_global_pattern) {
702 pcre2_code *code = NULL;
703 pcre2_match_data *match_data = NULL;
704 char fixed_string_pattern[LOG_GREP_PATTERN_BUFFER_SIZE] = {0};
705 bool is_fixed_string_pattern = false;
706 bool case_insensitive_fixed = false;
707
708 // Get code from CLI pattern or interactive grep pattern
709 if (global_pat && global_pat->is_fixed_string) {
710 SAFE_STRNCPY(fixed_string_pattern, global_pat->parsed_pattern, sizeof(fixed_string_pattern));
711 is_fixed_string_pattern = true;
712 case_insensitive_fixed = global_pat->case_insensitive;
713 } else if (global_pat && global_pat->compiled) {
714 code = global_pat->compiled;
715 match_data = get_thread_match_data();
716 } else if (is_log_search) {
717 // Interactive grep global highlighting - get pattern from grep state
718 code = (pcre2_code *)log_search_copy_pattern();
719 if (code) {
720 match_data = pcre2_match_data_create_from_pattern(code, NULL);
721 } else {
722 // No regex pattern - check if it's a fixed string pattern in interactive grep
723 int pattern_len = log_search_get_input_len();
724 const char *pattern = log_search_get_input_buffer();
725 if (pattern_len > 0 && pattern && pattern_len < (int)sizeof(fixed_string_pattern)) {
726 grep_parse_result_t parsed = grep_parse_pattern(pattern);
727 const char *literal = parsed.valid ? parsed.pattern : pattern;
728 SAFE_STRNCPY(fixed_string_pattern, literal, sizeof(fixed_string_pattern) - 1);
729 is_fixed_string_pattern = true;
730 case_insensitive_fixed = log_search_get_case_insensitive();
731 }
732 }
733 }
734
735 if ((!code || !match_data) && !is_fixed_string_pattern) {
736 if (match_data && is_log_search) {
737 pcre2_match_data_free(match_data); // Free if we created it
738 }
739 if (is_log_search && !global_pat && code)
740 pcre2_code_free(code);
741 return colored_text; // Fall back to no highlighting
742 }
743
744 size_t plain_len = strlen(plain_text);
745 size_t colored_len = strlen(colored_text);
746 char *dst = highlight_buffer;
747 size_t plain_offset = 0;
748 size_t colored_pos = 0;
749
750 // Handle fixed string global matching
751 if (is_fixed_string_pattern) {
752 while (plain_offset < plain_len) {
753 // Find next occurrence of fixed string
754 const char *found = NULL;
755 if (case_insensitive_fixed) {
756 found = utf8_strcasestr(plain_text + plain_offset, fixed_string_pattern);
757 } else {
758 found = strstr(plain_text + plain_offset, fixed_string_pattern);
759 }
760
761 if (!found) {
762 break; // No more matches
763 }
764
765 size_t plain_match_start = (size_t)(found - plain_text);
766 size_t plain_match_end = plain_match_start + strlen(fixed_string_pattern);
767
768 // Map to colored text positions (same as single-match path)
769 size_t colored_match_start = map_plain_to_colored_pos(colored_text, plain_match_start);
770 size_t colored_match_end = map_plain_to_colored_pos(colored_text, plain_match_end);
771
772 // Reserve the untouched tail plus the next background/reset sequences.
773 if ((size_t)(dst - highlight_buffer) + colored_len - colored_pos + 37 >= sizeof(highlight_buffer)) {
774 return colored_text;
775 }
776
777 // Copy text before match
778 if (colored_match_start > colored_pos) {
779 memcpy(dst, colored_text + colored_pos, colored_match_start - colored_pos);
780 dst += (colored_match_start - colored_pos);
781 }
782
783 // Add highlight background
784 uint8_t r, g, b;
785 get_highlight_color(&r, &g, &b);
786 dst = append_truecolor_bg(dst, r, g, b);
787
788 // Copy matched text with background
789 size_t match_byte_len = colored_match_end - colored_match_start;
790 memcpy(dst, colored_text + colored_match_start, match_byte_len);
791 dst += match_byte_len;
792
793 // Reset background
794 memcpy(dst, "\x1b[49m", 5);
795 dst += 5;
796
797 colored_pos = colored_match_end;
798 plain_offset = plain_match_end;
799 }
800
801 // Copy remaining text
802 if (colored_pos < colored_len) {
803 memcpy(dst, colored_text + colored_pos, colored_len - colored_pos);
804 dst += (colored_len - colored_pos);
805 }
806
807 // Reset both background and foreground at end
808 *dst = '\0';
809 return highlight_buffer;
810 }
811
812 // Find all matches in plain text and highlight in colored text (regex matching)
813 while (plain_offset < plain_len) {
814 int rc = asciichat_pcre2_match(code, (PCRE2_SPTR)plain_text, plain_len, plain_offset, 0, match_data, NULL);
815 if (rc < 0) {
816 break; // No more matches
817 }
818
819 PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
820 size_t plain_match_start = (size_t)ovector[0];
821 size_t plain_match_end = (size_t)ovector[1];
822
823 // Map to colored text positions
824 size_t colored_match_start = map_plain_to_colored_pos(colored_text, plain_match_start);
825 size_t colored_match_end = map_plain_to_colored_pos(colored_text, plain_match_end);
826
827 // Copy text before match
828 if (colored_match_start > colored_pos) {
829 memcpy(dst, colored_text + colored_pos, colored_match_start - colored_pos);
830 dst += (colored_match_start - colored_pos);
831 }
832
833 // Add highlight background
834 uint8_t r, g, b;
835 get_highlight_color(&r, &g, &b);
836 dst = append_truecolor_bg(dst, r, g, b);
837
838 // Copy matched text, re-applying background after any [0m or [00m reset codes
839 size_t match_byte_len = colored_match_end - colored_match_start;
840 const char *match_src = colored_text + colored_match_start;
841 char *dst_end = highlight_buffer + sizeof(highlight_buffer) - 1; // Absolute buffer end
842 size_t i = 0;
843 while (i < match_byte_len) {
844 // Check for [0m or [00m reset codes
845 if (i + 4 <= match_byte_len && match_src[i] == '\x1b' && match_src[i + 1] == '[' && match_src[i + 2] == '0' &&
846 match_src[i + 3] == 'm') {
847 // Found [0m - copy it and re-apply background (if room)
848 if (dst + 4 >= dst_end)
849 break; // Not enough space for the reset code itself
850 *dst++ = match_src[i++]; // ESC
851 *dst++ = match_src[i++]; // '['
852 *dst++ = match_src[i++]; // '0'
853 *dst++ = match_src[i++]; // 'm'
854 // Re-apply highlight background after reset (only if room)
855 if (dst + 20 < dst_end) {
856 uint8_t r2, g2, b2;
857 get_highlight_color(&r2, &g2, &b2);
858 dst = append_truecolor_bg(dst, r2, g2, b2);
859 }
860 } else if (i + 5 <= match_byte_len && match_src[i] == '\x1b' && match_src[i + 1] == '[' &&
861 match_src[i + 2] == '0' && match_src[i + 3] == '0' && match_src[i + 4] == 'm') {
862 // Found [00m - copy it and re-apply background (if room)
863 if (dst + 5 >= dst_end)
864 break; // Not enough space for the reset code itself
865 *dst++ = match_src[i++]; // ESC
866 *dst++ = match_src[i++]; // '['
867 *dst++ = match_src[i++]; // '0'
868 *dst++ = match_src[i++]; // '0'
869 *dst++ = match_src[i++]; // 'm'
870 // Re-apply highlight background after reset (only if room)
871 if (dst + 20 < dst_end) {
872 uint8_t r2, g2, b2;
873 get_highlight_color(&r2, &g2, &b2);
874 dst = append_truecolor_bg(dst, r2, g2, b2);
875 }
876 } else {
877 // Regular character - just copy
878 if (dst + 1 >= dst_end)
879 break;
880 *dst++ = match_src[i++];
881 }
882 }
883
884 // Reset background only
885 memcpy(dst, "\x1b[49m", 5);
886 dst += 5;
887
888 colored_pos = colored_match_end;
889 plain_offset = plain_match_end;
890
891 // Prevent infinite loop on zero-length matches
892 if (plain_match_end == plain_match_start) {
893 plain_offset++;
894 }
895 }
896
897 // Copy remaining text
898 if (colored_pos < colored_len) {
899 memcpy(dst, colored_text + colored_pos, colored_len - colored_pos);
900 dst += (colored_len - colored_pos);
901 }
902
903 // Reset both background and foreground at end to prevent color bleeding
904 *dst = '\0';
905
906 // Clean up match data if we created it for interactive grep
907 if (is_log_search && match_data && !global_pat) {
908 pcre2_match_data_free(match_data);
909 }
910 if (is_log_search && !global_pat && code)
911 pcre2_code_free(code);
912
913 return highlight_buffer;
914 }
915
916 // Single match highlighting (same logic as global path)
917 // map_plain_to_colored_pos(N) returns byte position after counting N plain bytes
918 size_t colored_start = map_plain_to_colored_pos(colored_text, match_start);
919 size_t colored_end = map_plain_to_colored_pos(colored_text, match_start + match_len);
920
921 size_t colored_len = strlen(colored_text);
922 char *dst = highlight_buffer;
923
924 // Copy everything before the match
925 if (colored_start > 0) {
926 memcpy(dst, colored_text, colored_start);
927 dst += colored_start;
928 }
929
930 // Add highlight background
931 uint8_t r, g, b;
932 get_highlight_color(&r, &g, &b);
933 dst = append_truecolor_bg(dst, r, g, b);
934
935 // Copy matched text, re-applying background after any [0m or [00m reset codes
936 size_t match_byte_len = colored_end - colored_start;
937 const char *match_src = colored_text + colored_start;
938 char *dst_end = highlight_buffer + sizeof(highlight_buffer) - 1; // Absolute buffer end
939 size_t i = 0;
940
941 while (i < match_byte_len) {
942 // Check for [0m or [00m reset codes
943 if (i + 4 <= match_byte_len && match_src[i] == '\x1b' && match_src[i + 1] == '[' && match_src[i + 2] == '0' &&
944 match_src[i + 3] == 'm') {
945 // Found [0m - copy it and re-apply background (if room)
946 if (dst + 4 >= dst_end)
947 break; // Not enough space for even the reset code
948 *dst++ = match_src[i++]; // ESC
949 *dst++ = match_src[i++]; // '['
950 *dst++ = match_src[i++]; // '0'
951 *dst++ = match_src[i++]; // 'm'
952 // Re-apply highlight background after reset (only if room)
953 if (dst + 20 < dst_end) {
954 uint8_t r2, g2, b2;
955 get_highlight_color(&r2, &g2, &b2);
956 dst = append_truecolor_bg(dst, r2, g2, b2);
957 }
958 } else if (i + 5 <= match_byte_len && match_src[i] == '\x1b' && match_src[i + 1] == '[' &&
959 match_src[i + 2] == '0' && match_src[i + 3] == '0' && match_src[i + 4] == 'm') {
960 // Found [00m - copy it and re-apply background (if room)
961 if (dst + 5 >= dst_end)
962 break; // Not enough space for even the reset code
963 *dst++ = match_src[i++]; // ESC
964 *dst++ = match_src[i++]; // '['
965 *dst++ = match_src[i++]; // '0'
966 *dst++ = match_src[i++]; // '0'
967 *dst++ = match_src[i++]; // 'm'
968 // Re-apply highlight background after reset (only if room)
969 if (dst + 20 < dst_end) {
970 uint8_t r3, g3, b3;
971 get_highlight_color(&r3, &g3, &b3);
972 dst = append_truecolor_bg(dst, r3, g3, b3);
973 }
974 } else {
975 // Regular character - just copy
976 if (dst + 1 >= dst_end) {
977 break;
978 }
979 *dst++ = match_src[i++];
980 }
981 }
982
983 // Reset only the background color to preserve foreground colors from headers
984 // Using \x1b[49m (reset background only) instead of \x1b[0m (reset all)
985 memcpy(dst, "\x1b[49m", 5);
986 dst += 5;
987
988 // Copy remaining text
989 size_t remaining = colored_len - colored_end;
990 if (remaining > 0) {
991 memcpy(dst, colored_text + colored_end, remaining);
992 dst += remaining;
993 }
994
995 *dst = '\0';
996 return highlight_buffer;
997}
grep_parse_result_t grep_parse_pattern(const char *input)
Parse pattern in /pattern/flags or pattern/flags format.
Definition grep.c:265
#define LOG_GREP_PATTERN_BUFFER_SIZE
Definition grep.h:9
char * append_truecolor_bg(char *dst, uint8_t r, uint8_t g, uint8_t b)
Definition ansi.c:145
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
unsigned char uint8_t
Definition common.h:60
#define LOG_TERMINAL_BUFFER_SIZE
Definition log/log.h:104
const char * utf8_strcasestr(const char *haystack, const char *needle)
Case-insensitive substring search with full Unicode support.
Definition utf8.c:205
int asciichat_pcre2_match(const pcre2_code *code, PCRE2_SPTR subject, PCRE2_SIZE length, PCRE2_SIZE startoffset, uint32_t options, pcre2_match_data *match_data, pcre2_match_context *context)
Definition pcre2.c:164
bool log_search_get_global_highlight(void)
Check if global highlighting (/g flag) is enabled in current pattern.
Definition search.c:945
int log_search_get_input_len(void)
Get current input buffer length (must hold mutex).
Definition search.c:989
const char * log_search_get_input_buffer(void)
Get current input buffer content (must hold mutex).
Definition search.c:997
bool log_search_is_active(void)
Check if filtering is active.
Definition search.c:413
bool log_search_get_case_insensitive(void)
Check if case-insensitive flag is set (must hold mutex).
Definition search.c:1001
void * log_search_copy_pattern(void)
Get the compiled regex pattern for interactive grep (internal use).
Definition search.c:960
Parsed pattern result from /pattern/flags syntax.
Definition grep.h:22
char pattern[4096]
Parsed pattern string.
Definition grep.h:23
bool valid
True if parsing succeeded.
Definition grep.h:31
bool case_insensitive
Case-insensitive matching (i flag).
Definition grep.c:48
bool is_fixed_string
True for fixed string matching (no regex).
Definition grep.c:47
char * parsed_pattern
Parsed pattern (without delimiters/flags).
Definition grep.c:45
pcre2_code * compiled
Compiled pattern (NULL if fixed string).
Definition grep.c:46

References append_truecolor_bg(), asciichat_pcre2_match(), grep_pattern_t::case_insensitive, grep_pattern_t::compiled, grep_parse_pattern(), grep_pattern_t::is_fixed_string, LOG_GREP_PATTERN_BUFFER_SIZE, log_search_copy_pattern(), log_search_get_case_insensitive(), log_search_get_global_highlight(), log_search_get_input_buffer(), log_search_get_input_len(), log_search_is_active(), LOG_TERMINAL_BUFFER_SIZE, grep_pattern_t::parsed_pattern, grep_parse_result_t::pattern, SAFE_STRNCPY, utf8_strcasestr(), and grep_parse_result_t::valid.

Referenced by grep_highlight(), and grep_highlight_colored_copy().

◆ grep_highlight_colored_copy()

char * grep_highlight_colored_copy ( const char * colored_text,
const char * plain_text,
size_t match_start,
size_t match_len )

Highlight matches in colored text, returning a new allocated copy.

Like grep_highlight_colored() but returns a newly allocated string instead of a static buffer. Caller must free the returned string with SAFE_FREE().

Parameters
colored_textOriginal text with ANSI color codes
plain_textStripped plain text (for match positions)
match_startMatch start position in plain text
match_lenMatch length in plain text
Returns
Newly allocated highlighted text (caller must free), or NULL on error

Definition at line 656 of file grep.c.

657 {
658 const char *result = grep_highlight_colored(colored_text, plain_text, match_start, match_len);
659 if (result) {
660 size_t result_len = strlen(result) + 1;
661 char *copy = SAFE_MALLOC(result_len, char *);
662 if (copy) {
663 memcpy(copy, result, result_len);
664 return copy;
665 }
666 }
667 return NULL;
668}
#define SAFE_MALLOC(size, cast)
Definition common.h:265

References grep_highlight_colored(), and SAFE_MALLOC.

◆ grep_init()

asciichat_error_t grep_init ( const char * pattern)

Initialize log filtering with PCRE2 regex pattern.

Compiles the pattern eagerly at startup. If compilation fails, logs a warning and disables filtering.

Parameters
patternPCRE2 regex pattern (NULL or empty = no filtering)
Returns
ASCIICHAT_OK on success, ERROR_INVALID_PARAM on bad regex

Definition at line 412 of file grep.c.

412 {
413 // Reject NULL or empty patterns
414 if (!pattern) {
415 return SET_ERRNO(ERROR_INVALID_PARAM, "Pattern cannot be NULL");
416 }
417 if (strlen(pattern) == 0) {
418 return SET_ERRNO(ERROR_INVALID_PARAM, "Pattern cannot be empty");
419 }
420
421 // Parse pattern
422 parse_result_t parsed = grep_parse_pattern(pattern);
423 if (!parsed.valid) {
424 log_error("Invalid --grep pattern format: \"%s\"", pattern);
425 log_error("Use /pattern/flags format (e.g., \"/query/ig\") or plain regex (e.g., \"query\")");
426 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid --grep pattern format");
427 }
428
429 // Allocate or expand pattern array
430 if (g_filter_state.pattern_count >= g_filter_state.pattern_capacity) {
431 int new_capacity = (g_filter_state.pattern_capacity == 0) ? 4 : g_filter_state.pattern_capacity * 2;
432 grep_pattern_t *new_patterns =
433 SAFE_REALLOC(g_filter_state.patterns, new_capacity * sizeof(grep_pattern_t), grep_pattern_t *);
434 if (!new_patterns) {
435 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate pattern array");
436 }
437 g_filter_state.patterns = new_patterns;
438 g_filter_state.pattern_capacity = new_capacity;
439 }
440
441 // Add new pattern
442 grep_pattern_t *new_pat = &g_filter_state.patterns[g_filter_state.pattern_count];
443 memset(new_pat, 0, sizeof(*new_pat));
444
445 // Make a copy of the original pattern string (argv pointers may be deallocated)
446 char *original_copy = SAFE_MALLOC(strlen(pattern) + 1, char *);
447 if (!original_copy) {
448 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate original pattern string");
449 }
450 memcpy(original_copy, pattern, strlen(pattern) + 1);
451 new_pat->original = original_copy;
452
453 new_pat->parsed_pattern = SAFE_MALLOC(strlen(parsed.pattern) + 1, char *);
454 if (!new_pat->parsed_pattern) {
455 SAFE_FREE(original_copy);
456 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate pattern string");
457 }
458 memcpy(new_pat->parsed_pattern, parsed.pattern, strlen(parsed.pattern) + 1);
459
460 new_pat->is_fixed_string = parsed.is_fixed_string;
461 new_pat->case_insensitive = parsed.case_insensitive;
462 new_pat->invert = parsed.invert;
463 new_pat->global_flag = parsed.global_flag;
464 new_pat->context_before = parsed.context_before;
465 new_pat->context_after = parsed.context_after;
466
467 // Compile regex (skip for fixed strings)
468 if (!parsed.is_fixed_string) {
469 new_pat->compiled = asciichat_pcre2_compile(parsed.pattern, parsed.pcre2_options);
470 if (!new_pat->compiled) {
471 SAFE_FREE(original_copy);
472 SAFE_FREE(new_pat->parsed_pattern);
473 return SET_ERRNO(ERROR_INVALID_PARAM, "Failed to compile regex pattern");
474 }
475
476 // Validate compilation
477 pcre2_code *code = new_pat->compiled;
478 if (!code) {
479 SAFE_FREE(new_pat->parsed_pattern);
480 return SET_ERRNO(ERROR_INVALID_PARAM, "Regex compilation failed");
481 }
482 }
483
484 g_filter_state.pattern_count++;
485
486 // Update context buffer size if needed
487 if (parsed.context_before > g_filter_state.buffer_size) {
488 // Reallocate line buffer
489 char **new_buffer = SAFE_REALLOC(g_filter_state.line_buffer, parsed.context_before * sizeof(char *), char **);
490 if (!new_buffer) {
491 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate context line buffer");
492 }
493 // Initialize new slots
494 for (int i = g_filter_state.buffer_size; i < parsed.context_before; i++) {
495 new_buffer[i] = NULL;
496 }
497 g_filter_state.line_buffer = new_buffer;
498 g_filter_state.buffer_size = parsed.context_before;
499 }
500
501 // Update max context_after
502 if (parsed.context_after > g_filter_state.max_context_after) {
503 g_filter_state.max_context_after = parsed.context_after;
504 }
505
506 g_filter_state.enabled = true;
507 log_info("Added --grep pattern #%d: %s", g_filter_state.pattern_count, pattern);
508
509 return ASCIICHAT_OK;
510}
#define SET_ERRNO(code,...)
grep_parse_result_t parse_result_t
Definition grep.c:222
int pattern_capacity
Allocated capacity.
Definition grep.c:61
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:285
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
pcre2_code * asciichat_pcre2_compile(const char *pattern, uint32_t flags)
Definition pcre2.c:16
int context_before
Lines before match (B<n> flag).
Definition grep.h:29
bool global_flag
Highlight all matches (g flag).
Definition grep.h:28
bool is_fixed_string
True if fixed string (not regex).
Definition grep.h:25
bool invert
Invert match (I flag).
Definition grep.h:27
bool case_insensitive
Case-insensitive (i flag).
Definition grep.h:26
int context_after
Lines after match (A<n> flag).
Definition grep.h:30
uint32_t pcre2_options
PCRE2 compile options.
Definition grep.h:24
int context_before
Lines before match (B flag).
Definition grep.c:51
int context_after
Lines after match (A flag).
Definition grep.c:52
bool global_flag
Highlight all matches (g flag).
Definition grep.c:50
bool invert
Invert match (I flag).
Definition grep.c:49

References ASCIICHAT_OK, asciichat_pcre2_compile(), grep_parse_result_t::case_insensitive, grep_pattern_t::case_insensitive, grep_pattern_t::compiled, grep_parse_result_t::context_after, grep_pattern_t::context_after, grep_parse_result_t::context_before, grep_pattern_t::context_before, ERROR_INVALID_PARAM, ERROR_MEMORY, grep_parse_result_t::global_flag, grep_pattern_t::global_flag, grep_parse_pattern(), grep_parse_result_t::invert, grep_pattern_t::invert, grep_parse_result_t::is_fixed_string, grep_pattern_t::is_fixed_string, log_error, log_info, grep_pattern_t::original, grep_pattern_t::parsed_pattern, grep_parse_result_t::pattern, grep_parse_result_t::pcre2_options, SAFE_FREE, SAFE_MALLOC, SAFE_REALLOC, SET_ERRNO, and grep_parse_result_t::valid.

Referenced by main().

◆ grep_parse_pattern()

grep_parse_result_t grep_parse_pattern ( const char * input)

Parse pattern in /pattern/flags format or plain regex format.

Supports two formats:

  • Format 1: /pattern/flags (e.g., "/error/igC2")
  • Format 2: plain pattern (e.g., "error")

Flags: i(case-insensitive), m(multiline), s(dotall), x(extended), g(global highlight), I(invert), F(fixed string), A<n>(after context), B<n>(before context), C<n>(both context)

Parameters
inputInput pattern string
Returns
Parsed result with pattern, flags, and validity

Parse pattern in /pattern/flags format or plain regex format.

Parameters
inputInput pattern string (either /pattern/flags, pattern/flags, or plain pattern)
Returns
Parse result with all extracted settings

Format 1 (with flags, explicit): /pattern/flags

  • Pattern enclosed in forward slashes with optional flags
  • Use F flag for fixed string (literal) matching

Format 2 (with flags, implicit): pattern/flags

  • Interactive grep format (no leading slash, since / starts the grep command)
  • Treats last slash as delimiter

Format 3 (plain regex): pattern

  • Plain regex pattern without slashes or flags
  • Treated as regex with default options (no flags)

Flags (formats 1 & 2):

  • i: case-insensitive
  • m: multiline mode (regex only)
  • s: dotall mode (regex only)
  • x: extended mode (regex only)
  • g: global (highlight all matches)
  • I: invert match (show non-matching lines)
  • F: fixed string (literal match, no regex)
  • A<n>: show n lines after match (e.g., A3)
  • B<n>: show n lines before match (e.g., B2)
  • C<n>: show n lines before and after match (e.g., C5)

Examples:

  • "/test/" - Simple regex with slashes
  • "test" - Plain regex without slashes
  • "/query/i" - Case-insensitive regex
  • "/test/F" - Fixed string match for "test"
  • "/api/v1/users/F" - Fixed string match for "api/v1/users"
  • "/ERROR/IA3" - Invert match + 3 lines after
  • "/FATAL/B2A5F" - Fixed string, 2 before, 5 after
  • "error|warn" - Plain regex with alternation
  • "search/i" - Interactive grep: case-insensitive "search"
  • "api/v1/users/F" - Interactive grep: fixed string match

Definition at line 265 of file grep.c.

265 {
266 parse_result_t result = {0};
267 result.pcre2_options = PCRE2_UTF | PCRE2_UCP; // Default: UTF-8 mode
268
269 if (!input || strlen(input) == 0) {
270 return result; // Invalid: empty pattern
271 }
272
273 size_t len = strlen(input);
274 const char *closing_slash = NULL;
275 const char *pattern_start = input;
276
277 // Check if pattern uses /pattern/flags format (explicit)
278 if (input[0] == '/') {
279 // Format 1: /pattern/flags
280 if (len < 3) {
281 return result; // Invalid: too short for /pattern/ format
282 }
283
284 // Find closing slash - don't treat backslash as an escape character
285 // (backslashes are regex escapes, not delimiter escapes)
286 closing_slash = strchr(input + 1, '/');
287 if (!closing_slash) {
288 return result; // Invalid: missing closing /
289 }
290
291 pattern_start = input + 1;
292 } else {
293 // Check for pattern/flags format (implicit, no leading slash)
294 // Find the last forward slash to use as delimiter
295 closing_slash = strrchr(input, '/');
296
297 const char *implicit_flags = closing_slash ? closing_slash + 1 : NULL;
298 bool implicit_flags_valid = closing_slash && closing_slash > input && *implicit_flags != '\0';
299 if (implicit_flags_valid) {
300 for (const char *flag = implicit_flags; *flag; flag++) {
301 if (!strchr("imsxgIF", *flag) && *flag != 'A' && *flag != 'B' && *flag != 'C' &&
302 !(*flag >= '0' && *flag <= '9')) {
303 implicit_flags_valid = false;
304 break;
305 }
306 }
307 }
308
309 if (implicit_flags_valid) {
310 // There's at least one slash (not at the beginning)
311 // Try to parse as pattern/flags format
312 pattern_start = input;
313 } else {
314 // No slash or slash at beginning - treat as plain pattern
315 closing_slash = NULL;
316 }
317 }
318
319 if (closing_slash) {
320 // Extract pattern between start and closing slash
321 size_t pattern_len = (size_t)(closing_slash - pattern_start);
322 if (pattern_len == 0) {
323 return result; // Invalid: empty pattern
324 }
325 if (pattern_len >= sizeof(result.pattern)) {
326 pattern_len = sizeof(result.pattern) - 1;
327 }
328 memcpy(result.pattern, pattern_start, pattern_len);
329 result.pattern[pattern_len] = '\0';
330
331 // Parse flags after closing slash
332 // First pass: check for F flag
333 const char *flags = closing_slash + 1;
334 bool has_F_flag = (strchr(flags, 'F') != NULL);
335
336 // Parse all flags
337 for (const char *p = flags; *p; p++) {
338 char c = *p;
339
340 // Single-character flags
341 if (c == 'i') {
342 result.pcre2_options |= PCRE2_CASELESS;
343 result.case_insensitive = true;
344 } else if (c == 'm') {
345 result.pcre2_options |= PCRE2_MULTILINE;
346 } else if (c == 's') {
347 result.pcre2_options |= PCRE2_DOTALL;
348 } else if (c == 'x') {
349 result.pcre2_options |= PCRE2_EXTENDED;
350 } else if (c == 'g') {
351 result.global_flag = true;
352 } else if (c == 'I') {
353 result.invert = true;
354 } else if (c == 'F') {
355 result.is_fixed_string = true;
356 }
357 // Multi-character flags with integers
358 else if (c == 'A') {
359 p++; // Move to digits
360 int num = 0;
361 while (*p >= '0' && *p <= '9') {
362 num = num * 10 + (*p - '0');
363 p++;
364 }
365 p--; // Back up one (for loop will increment)
366 result.context_after = (num > 0) ? num : 1; // Default to 1 if no number
367 } else if (c == 'B') {
368 p++;
369 int num = 0;
370 while (*p >= '0' && *p <= '9') {
371 num = num * 10 + (*p - '0');
372 p++;
373 }
374 p--;
375 result.context_before = (num > 0) ? num : 1;
376 } else if (c == 'C') {
377 p++;
378 int num = 0;
379 while (*p >= '0' && *p <= '9') {
380 num = num * 10 + (*p - '0');
381 p++;
382 }
383 p--;
384 int ctx = (num > 0) ? num : 1;
385 result.context_before = ctx;
386 result.context_after = ctx;
387 } else {
388 // Invalid flag character - only error if not using F flag
389 if (!has_F_flag) {
390 return result; // Invalid
391 }
392 // Otherwise ignore invalid flags (they're part of the fixed string context)
393 }
394 }
395 } else {
396 // Format 3: Plain pattern without slashes (treat as regex, no flags)
397 size_t pattern_len = len;
398 if (pattern_len >= sizeof(result.pattern)) {
399 pattern_len = sizeof(result.pattern) - 1;
400 }
401 memcpy(result.pattern, input, pattern_len);
402 result.pattern[pattern_len] = '\0';
403
404 // No flags for plain format - just use default options
405 // (PCRE2_UTF | PCRE2_UCP already set at the top)
406 }
407
408 result.valid = true;
409 return result;
410}

References grep_parse_result_t::case_insensitive, grep_parse_result_t::context_after, grep_parse_result_t::context_before, grep_parse_result_t::global_flag, grep_parse_result_t::invert, grep_parse_result_t::is_fixed_string, grep_parse_result_t::pattern, grep_parse_result_t::pcre2_options, and grep_parse_result_t::valid.

Referenced by grep_highlight_colored(), grep_init(), log_search_enter_mode(), log_search_exit_mode(), log_search_filter_buffer(), log_search_get_match_info(), and log_search_init().

◆ grep_restore_patterns()

asciichat_error_t grep_restore_patterns ( void )

Restore previously saved filter patterns.

Restores filter patterns saved by grep_save_patterns(). Used by interactive grep when user cancels (Escape) to restore CLI –grep patterns.

Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 1108 of file grep.c.

1108 {
1109 grep_pattern_t *copy = NULL;
1110 asciichat_error_t result =
1111 grep_copy_patterns(g_filter_state.saved_patterns, g_filter_state.saved_pattern_count, &copy);
1112 if (result != ASCIICHAT_OK)
1113 return result;
1114 for (int i = 0; i < g_filter_state.pattern_count; i++)
1115 grep_free_pattern(&g_filter_state.patterns[i]);
1116 SAFE_FREE(g_filter_state.patterns);
1117 g_filter_state.patterns = copy;
1118 g_filter_state.pattern_count = g_filter_state.saved_pattern_count;
1119 g_filter_state.pattern_capacity = g_filter_state.saved_pattern_count;
1120 g_filter_state.enabled = g_filter_state.saved_enabled;
1121 return ASCIICHAT_OK;
1122}
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44

References ASCIICHAT_OK, and SAFE_FREE.

Referenced by log_search_exit_mode().

◆ grep_save_patterns()

asciichat_error_t grep_save_patterns ( void )

Save current filter patterns for later restoration.

Saves the current state of CLI –grep patterns so they can be restored later. Used by interactive grep to preserve initial filters when entering interactive mode.

Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 1093 of file grep.c.

1093 {
1094 grep_pattern_t *copy = NULL;
1095 asciichat_error_t result = grep_copy_patterns(g_filter_state.patterns, g_filter_state.pattern_count, &copy);
1096 if (result != ASCIICHAT_OK)
1097 return result;
1098 for (int i = 0; i < g_filter_state.saved_pattern_count; i++)
1099 grep_free_pattern(&g_filter_state.saved_patterns[i]);
1100 SAFE_FREE(g_filter_state.saved_patterns);
1101 g_filter_state.saved_patterns = copy;
1102 g_filter_state.saved_pattern_count = g_filter_state.pattern_count;
1103 g_filter_state.saved_pattern_capacity = g_filter_state.pattern_count;
1104 g_filter_state.saved_enabled = g_filter_state.enabled;
1105 return ASCIICHAT_OK;
1106}

References ASCIICHAT_OK, and SAFE_FREE.

Referenced by log_search_enter_mode().

◆ grep_should_output()

bool grep_should_output ( const char * log_line,
size_t * match_start,
size_t * match_len )

Check if log line should be output (matches filter).

Thread-safe: Uses thread-local match_data storage. Non-allocating: Reuses pre-allocated match_data.

Parameters
log_lineFormatted log line to check
match_startOutput: start offset of match
match_lenOutput: length of match
Returns
true if line should be output, false to suppress

Definition at line 512 of file grep.c.

512 {
513 // Reject NULL lines
514 if (!log_line) {
515 if (match_start)
516 *match_start = 0;
517 if (match_len)
518 *match_len = 0;
519 return false;
520 }
521
522 // If filtering disabled, output everything
523 if (!g_filter_state.enabled) {
524 return true;
525 }
526
527 // Check if we're in "context after" mode (outputting lines after a match)
528 if (g_filter_state.lines_after_match > 0) {
529 g_filter_state.lines_after_match--;
530 *match_start = 0;
531 *match_len = 0;
532
533 // Store in buffer even during context-after mode
534 if (g_filter_state.buffer_size > 0) {
535 SAFE_FREE(g_filter_state.line_buffer[g_filter_state.buffer_pos]);
536 size_t line_len = strlen(log_line) + 1;
537 g_filter_state.line_buffer[g_filter_state.buffer_pos] = SAFE_MALLOC(line_len, char *);
538 if (g_filter_state.line_buffer[g_filter_state.buffer_pos]) {
539 memcpy(g_filter_state.line_buffer[g_filter_state.buffer_pos], log_line, line_len);
540 }
541 g_filter_state.buffer_pos = (g_filter_state.buffer_pos + 1) % g_filter_state.buffer_size;
542 }
543
544 return true; // Output context line
545 }
546
547 pcre2_match_data *match_data = get_thread_match_data();
548 size_t line_len = strlen(log_line);
549
550 // Try each pattern (OR logic)
551 bool any_match = false;
552 grep_pattern_t *matched_pattern = NULL;
553
554 for (int i = 0; i < g_filter_state.pattern_count; i++) {
555 grep_pattern_t *pat = &g_filter_state.patterns[i];
556 bool this_match = false;
557
558 if (pat->is_fixed_string) {
559 // Fixed string matching (with optional case-insensitive support)
560 const char *found;
561 if (pat->case_insensitive) {
562 // Unicode-aware case-insensitive search
563 found = utf8_strcasestr(log_line, pat->parsed_pattern);
564 } else {
565 // Case-sensitive search
566 found = strstr(log_line, pat->parsed_pattern);
567 }
568
569 if (found) {
570 this_match = true;
571 if (!matched_pattern) {
572 *match_start = (size_t)(found - log_line);
573 *match_len = strlen(pat->parsed_pattern);
574 matched_pattern = pat;
575 }
576 }
577 } else {
578 // Regex matching
579 if (!match_data || !pat->compiled) {
580 continue;
581 }
582
583 pcre2_code *code = pat->compiled;
584 if (!code) {
585 continue;
586 }
587
588 int rc = asciichat_pcre2_match(code, (PCRE2_SPTR)log_line, line_len, 0, 0, match_data, NULL);
589 if (rc >= 0) {
590 this_match = true;
591 if (!matched_pattern) {
592 PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
593 *match_start = (size_t)ovector[0];
594 *match_len = (size_t)(ovector[1] - ovector[0]);
595 matched_pattern = pat;
596 }
597 }
598 }
599
600 // Apply invert flag
601 if (pat->invert) {
602 this_match = !this_match;
603 }
604
605 if (this_match) {
606 any_match = true;
607 // Update matched_pattern if this one has more context
608 if (!matched_pattern || (pat->context_before + pat->context_after) >
609 (matched_pattern->context_before + matched_pattern->context_after)) {
610 matched_pattern = pat;
611 }
612 }
613 }
614
615 // If any pattern matched
616 if (any_match && matched_pattern) {
617 // Output context_before lines from circular buffer
618 if (matched_pattern->context_before > 0 && g_filter_state.buffer_size > 0) {
619 int num_lines = (matched_pattern->context_before < g_filter_state.buffer_size) ? matched_pattern->context_before
620 : g_filter_state.buffer_size;
621
622 // Calculate starting position in circular buffer
623 int start_pos = (g_filter_state.buffer_pos - num_lines + g_filter_state.buffer_size) % g_filter_state.buffer_size;
624
625 // Output buffered lines
626 for (int i = 0; i < num_lines; i++) {
627 int pos = (start_pos + i) % g_filter_state.buffer_size;
628 if (g_filter_state.line_buffer[pos]) {
629 fprintf(stderr, "%s\n", g_filter_state.line_buffer[pos]);
630 }
631 }
632 }
633
634 // Set up context_after counter
635 if (matched_pattern->context_after > 0) {
636 g_filter_state.lines_after_match = matched_pattern->context_after;
637 }
638
639 return true; // Match found, output line
640 }
641
642 // No match - store in context buffer for potential future use
643 if (g_filter_state.buffer_size > 0) {
644 SAFE_FREE(g_filter_state.line_buffer[g_filter_state.buffer_pos]);
645 size_t line_len_copy = strlen(log_line) + 1;
646 g_filter_state.line_buffer[g_filter_state.buffer_pos] = SAFE_MALLOC(line_len_copy, char *);
647 if (g_filter_state.line_buffer[g_filter_state.buffer_pos]) {
648 memcpy(g_filter_state.line_buffer[g_filter_state.buffer_pos], log_line, line_len_copy);
649 }
650 g_filter_state.buffer_pos = (g_filter_state.buffer_pos + 1) % g_filter_state.buffer_size;
651 }
652
653 return false; // No match, suppress line
654}
int buffer_size
Size of circular buffer.
Definition grep.c:68

References asciichat_pcre2_match(), grep_pattern_t::case_insensitive, grep_pattern_t::compiled, grep_pattern_t::context_after, grep_pattern_t::context_before, grep_pattern_t::invert, grep_pattern_t::is_fixed_string, grep_pattern_t::parsed_pattern, SAFE_FREE, SAFE_MALLOC, and utf8_strcasestr().