grep.c: teach 'git grep --only-matching'
authorTaylor Blau <me@ttaylorr.com>
Mon, 9 Jul 2018 20:33:47 +0000 (15:33 -0500)
committerJunio C Hamano <gitster@pobox.com>
Mon, 9 Jul 2018 21:15:28 +0000 (14:15 -0700)
Teach 'git grep --only-matching', a new option to only print the
matching part(s) of a line.

For instance, a line containing the following (taken from README.md:27):

(`man gitcvs-migration` or `git help cvs-migration` if git is

Is printed as follows:

$ git grep --line-number --column --only-matching -e git -- \
README.md | grep ":27"
README.md:27:7:git
README.md:27:16:git
README.md:27:38:git

The patch works mostly as one would expect, with the exception of a few
considerations that are worth mentioning here.

Like GNU grep, this patch ignores --only-matching when --invert (-v) is
given. There is a sensible answer here, but parity with the behavior of
other tools is preferred.

Because a line might contain more than one match, there are special
considerations pertaining to when to print line headers, newlines, and
how to increment the match column offset. The line header and newlines
are handled as a special case within the main loop to avoid polluting
the surrounding code with conditionals that have large blocks.

Signed-off-by: Taylor Blau <me@ttaylorr.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/git-grep.txt
builtin/grep.c
grep.c
grep.h
t/t7810-grep.sh
index 0de3493b804b3f71fb5c9fc66dbf2a75c6ab0f0c..a3049af1a36c09325a88ad2cc822ae2891c8974b 100644 (file)
@@ -17,7 +17,7 @@ SYNOPSIS
           [-l | --files-with-matches] [-L | --files-without-match]
           [(-O | --open-files-in-pager) [<pager>]]
           [-z | --null]
-          [-c | --count] [--all-match] [-q | --quiet]
+          [ -o | --only-matching ] [-c | --count] [--all-match] [-q | --quiet]
           [--max-depth <depth>]
           [--color[=<when>] | --no-color]
           [--break] [--heading] [-p | --show-function]
@@ -201,6 +201,11 @@ providing this option will cause it to die.
        Output \0 instead of the character that normally follows a
        file name.
 
+-o::
+--only-matching::
+       Print only the matched (non-empty) parts of a matching line, with each such
+       part on a separate output line.
+
 -c::
 --count::
        Instead of showing every matched line, show the number of
index 61bcaf6e58d67c8bc66f6d8d260e91e7804caa5b..228b83990f990e8cd6f2a6bcbe05df0955921eda 100644 (file)
@@ -843,6 +843,8 @@ int cmd_grep(int argc, const char **argv, const char *prefix)
                OPT_BOOL_F('z', "null", &opt.null_following_name,
                           N_("print NUL after filenames"),
                           PARSE_OPT_NOCOMPLETE),
+               OPT_BOOL('o', "only-matching", &opt.only_matching,
+                       N_("show only matching parts of a line")),
                OPT_BOOL('c', "count", &opt.count,
                        N_("show the number of matches instead of matching lines")),
                OPT__COLOR(&opt.color, N_("highlight matches")),
@@ -962,6 +964,10 @@ int cmd_grep(int argc, const char **argv, const char *prefix)
        if (!opt.pattern_list)
                die(_("no pattern given."));
 
+       /* --only-matching has no effect with --invert. */
+       if (opt.invert)
+               opt.only_matching = 0;
+
        /*
         * We have to find "--" in a separate pass, because its presence
         * influences how we will parse arguments that come before it.
diff --git a/grep.c b/grep.c
index 4ff8a73043b194dbeb5fc7bd011e5931ad734846..49a744f96b373c6b90cde4d2d090eea2aa18cbc3 100644 (file)
--- a/grep.c
+++ b/grep.c
@@ -51,6 +51,7 @@ void init_grep_defaults(void)
        color_set(opt->color_match_selected, GIT_COLOR_BOLD_RED);
        color_set(opt->color_selected, "");
        color_set(opt->color_sep, GIT_COLOR_CYAN);
+       opt->only_matching = 0;
        opt->color = -1;
        opt->output = std_output;
 }
@@ -158,6 +159,7 @@ void grep_init(struct grep_opt *opt, const char *prefix)
        opt->pattern_tail = &opt->pattern_list;
        opt->header_tail = &opt->header_list;
 
+       opt->only_matching = def->only_matching;
        opt->color = def->color;
        opt->extended_regexp_option = def->extended_regexp_option;
        opt->pattern_type_option = def->pattern_type_option;
@@ -1446,7 +1448,8 @@ static void show_line(struct grep_opt *opt, char *bol, char *eol,
                      const char *name, unsigned lno, ssize_t cno, char sign)
 {
        int rest = eol - bol;
-       const char *match_color, *line_color = NULL;
+       const char *match_color = NULL;
+       const char *line_color = NULL;
 
        if (opt->file_break && opt->last_shown == 0) {
                if (opt->show_hunk_mark)
@@ -1462,39 +1465,55 @@ static void show_line(struct grep_opt *opt, char *bol, char *eol,
                        opt->output(opt, "\n", 1);
                }
        }
-       show_line_header(opt, name, lno, cno, sign);
-       if (opt->color) {
+       if (!opt->only_matching) {
+               /*
+                * In case the line we're being called with contains more than
+                * one match, leave printing each header to the loop below.
+                */
+               show_line_header(opt, name, lno, cno, sign);
+       }
+       if (opt->color || opt->only_matching) {
                regmatch_t match;
                enum grep_context ctx = GREP_CONTEXT_BODY;
                int ch = *eol;
                int eflags = 0;
 
-               if (sign == ':')
-                       match_color = opt->color_match_selected;
-               else
-                       match_color = opt->color_match_context;
-               if (sign == ':')
-                       line_color = opt->color_selected;
-               else if (sign == '-')
-                       line_color = opt->color_context;
-               else if (sign == '=')
-                       line_color = opt->color_function;
+               if (opt->color) {
+                       if (sign == ':')
+                               match_color = opt->color_match_selected;
+                       else
+                               match_color = opt->color_match_context;
+                       if (sign == ':')
+                               line_color = opt->color_selected;
+                       else if (sign == '-')
+                               line_color = opt->color_context;
+                       else if (sign == '=')
+                               line_color = opt->color_function;
+               }
                *eol = '\0';
                while (next_match(opt, bol, eol, ctx, &match, eflags)) {
                        if (match.rm_so == match.rm_eo)
                                break;
 
-                       output_color(opt, bol, match.rm_so, line_color);
+                       if (opt->only_matching)
+                               show_line_header(opt, name, lno, cno, sign);
+                       else
+                               output_color(opt, bol, match.rm_so, line_color);
                        output_color(opt, bol + match.rm_so,
                                     match.rm_eo - match.rm_so, match_color);
+                       if (opt->only_matching)
+                               opt->output(opt, "\n", 1);
                        bol += match.rm_eo;
+                       cno += match.rm_eo;
                        rest -= match.rm_eo;
                        eflags = REG_NOTBOL;
                }
                *eol = ch;
        }
-       output_color(opt, bol, rest, line_color);
-       opt->output(opt, "\n", 1);
+       if (!opt->only_matching) {
+               output_color(opt, bol, rest, line_color);
+               opt->output(opt, "\n", 1);
+       }
 }
 
 #ifndef NO_PTHREADS
diff --git a/grep.h b/grep.h
index 08a0b391c5bdf7a22055486d3b4fc76d41fd8e43..4d474d8ec439fa0256c548055b17cf1ce64505da 100644 (file)
--- a/grep.h
+++ b/grep.h
@@ -150,6 +150,7 @@ struct grep_opt {
        int relative;
        int pathname;
        int null_following_name;
+       int only_matching;
        int color;
        int max_depth;
        int funcname;
index 9312c8daf523abd3da1681e8777648c71401e2ae..d8c232dbf4d894a199b2db9df493fd4a810dc58c 100755 (executable)
@@ -262,6 +262,21 @@ do
                fi
        '
 
+       test_expect_success "grep $L (with --column, --only-matching)" '
+               {
+                       echo ${HC}file:1:5:mmap
+                       echo ${HC}file:2:5:mmap
+                       echo ${HC}file:3:5:mmap
+                       echo ${HC}file:3:13:mmap
+                       echo ${HC}file:4:5:mmap
+                       echo ${HC}file:4:13:mmap
+                       echo ${HC}file:5:5:mmap
+                       echo ${HC}file:5:13:mmap
+               } >expected &&
+               git grep --column -n -o -e mmap $H >actual &&
+               test_cmp expected actual
+       '
+
        test_expect_success "grep $L (t-1)" '
                echo "${HC}t/t:1:test" >expected &&
                git grep -n -e test $H >actual &&