Teach GNU grep(1)'s '-o' ('--only-matching') to 'git-grep'. This option
prints only the matching components of each line. It writes multiple
lines if more than one match exists on a given line.

For example:

  $ /git grep -on --column git -- README.md | head -3
  README.md:15:56:git
  README.md:18:20:git
  README.md:19:16:git

By using show_line_header(), 'git grep --only-matching' correctly
respects the '--heading' option:

  $ git grep -on --column --heading git -- README.md | head -4
  README.md
  15:56:git
  18:20:git
  19:16:git

We mirror GNU grep's behavior when given -A, -B, or -C with
--only-matching, by displaying only the matching portion(s) of a line,
ignoring contextual line(s), but displaying '--' (context separator)
line(s).

Notably: when show_line() is called on a line that contains _multiple_
matches, we keep track of a relative offset from the beginning of the
line and increment 'cno' in subsequent calls to show_line_header() when
the expression is not extended. In the extended case, we do not do this,
because the column of the first match is undefined, thus relative
offsets are meaningless.

Signed-off-by: Taylor Blau <m...@ttaylorr.com>
---
 Documentation/git-grep.txt |  6 +++-
 builtin/grep.c             |  1 +
 grep.c                     | 34 +++++++++++++++++--
 grep.h                     |  1 +
 t/t7810-grep.sh            | 69 ++++++++++++++++++++++++++++++++++++++
 5 files changed, 107 insertions(+), 4 deletions(-)

diff --git a/Documentation/git-grep.txt b/Documentation/git-grep.txt
index c48a578cb1..5c09abec4a 100644
--- a/Documentation/git-grep.txt
+++ b/Documentation/git-grep.txt
@@ -20,7 +20,7 @@ SYNOPSIS
           [-c | --count] [--all-match] [-q | --quiet]
           [--max-depth <depth>]
           [--color[=<when>] | --no-color]
-          [--break] [--heading] [-p | --show-function]
+          [--break] [--heading] [-o | --only-matching] [-p | --show-function]
           [-A <post-context>] [-B <pre-context>] [-C <context>]
           [-W | --function-context]
           [--threads <num>]
@@ -223,6 +223,10 @@ providing this option will cause it to die.
        Show the filename above the matches in that file instead of
        at the start of each shown line.
 
+--o::
+--only-matching::
+       Prints only the matching part of the lines.
+
 -p::
 --show-function::
        Show the preceding line that contains the function name of
diff --git a/builtin/grep.c b/builtin/grep.c
index f9f516dfc4..0507ac335a 100644
--- a/builtin/grep.c
+++ b/builtin/grep.c
@@ -851,6 +851,7 @@ int cmd_grep(int argc, const char **argv, const char 
*prefix)
                        N_("print empty line between matches from different 
files")),
                OPT_BOOL(0, "heading", &opt.heading,
                        N_("show filename only once above matches from same 
file")),
+               OPT_BOOL('o', "only-matching", &opt.only_matching, N_("show 
only matches")),
                OPT_GROUP(""),
                OPT_CALLBACK('C', "context", &opt, N_("n"),
                        N_("show <n> context lines before and after matches"),
diff --git a/grep.c b/grep.c
index 36bf7cf08d..9297fde643 100644
--- a/grep.c
+++ b/grep.c
@@ -1422,12 +1422,24 @@ static void show_line(struct grep_opt *opt, char *bol, 
char *eol,
                        opt->output(opt, "\n", 1);
                }
        }
+       if (opt->only_matching && sign != ':') {
+               /*
+                * If we're given '--only-matching' and the line is a contextual
+                * one (i.e., we're given '-A', '-B', or '-C'), mark the line as
+                * shown (to advance opt->last_shown), but do not show it (since
+                * we are given '--only-matching').
+                */
+               opt->last_shown = lno;
+               return;
+       }
        show_line_header(opt, name, lno, cno, sign);
-       if (opt->color) {
+       if (opt->color || opt->only_matching) {
                regmatch_t match;
                enum grep_context ctx = GREP_CONTEXT_BODY;
                int ch = *eol;
                int eflags = 0;
+               int first = 1;
+               int offset = 1;
 
                if (sign == ':')
                        match_color = opt->color_match_selected;
@@ -1444,16 +1456,32 @@ static void show_line(struct grep_opt *opt, char *bol, 
char *eol,
                        if (match.rm_so == match.rm_eo)
                                break;
 
-                       output_color(opt, bol, match.rm_so, line_color);
+                       if (!opt->only_matching) {
+                               output_color(opt, bol, match.rm_so, line_color);
+                       } else if (!first) {
+                               /*
+                                * We are given --only-matching, and this is not
+                                * the first match on a line. Reprint the
+                                * newline and header before showing another
+                                * match.
+                                */
+                               opt->output(opt, "\n", 1);
+                               show_line_header(opt, name, lno,
+                                       opt->extended ? 0 : offset+match.rm_so,
+                                       sign);
+                       }
                        output_color(opt, bol + match.rm_so,
                                     match.rm_eo - match.rm_so, match_color);
+                       offset += match.rm_eo;
                        bol += match.rm_eo;
                        rest -= match.rm_eo;
                        eflags = REG_NOTBOL;
+                       first = 0;
                }
                *eol = ch;
        }
-       output_color(opt, bol, rest, line_color);
+       if (!opt->only_matching)
+               output_color(opt, bol, rest, line_color);
        opt->output(opt, "\n", 1);
 }
 
diff --git a/grep.h b/grep.h
index 08a0b391c5..24c1460100 100644
--- a/grep.h
+++ b/grep.h
@@ -126,6 +126,7 @@ struct grep_opt {
        const char *prefix;
        int prefix_length;
        regex_t regexp;
+       int only_matching;
        int linenum;
        int columnnum;
        int invert;
diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh
index 491b2e044a..6251ae678a 100755
--- a/t/t7810-grep.sh
+++ b/t/t7810-grep.sh
@@ -1432,6 +1432,75 @@ test_expect_success 'grep --heading' '
        test_cmp expected actual
 '
 
+test_expect_success 'grep --only-matching' '
+       cat >expected <<-\EOF &&
+       file:1:5:mmap
+       file:2:5:mmap
+       file:3:5:mmap
+       file:3:14:mmap
+       file:4:5:mmap
+       file:4:14:mmap
+       file:5:5:mmap
+       file:5:14:mmap
+       EOF
+       git grep --only-matching --line-number --column mmap file >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'grep --only-matching --column (unsupported)' '
+       cat >expected <<-\EOF &&
+       file:mmap
+       file:mmap
+       file:mmap
+       file:mmap
+       file:mmap
+       file:mmap
+       file:mmap
+       file:mmap
+       EOF
+       git grep --only-matching --column --not --not -e mmap -- file >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'grep --only-matching -C' '
+       cat >expected <<-\EOF &&
+       hello.ps1:function
+       hello.ps1:function
+       --
+       hello.ps1:function
+       EOF
+       git grep --only-matching -C1 function hello.ps1 >actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'grep --only-matching --heading' '
+       cat >expected <<-\EOF &&
+       file
+       1:5:mmap
+       2:5:mmap
+       3:5:mmap
+       3:14:mmap
+       4:5:mmap
+       4:14:mmap
+       5:5:mmap
+       5:14:mmap
+       EOF
+       git grep --only-matching --heading --line-number --column mmap file 
>actual &&
+       test_cmp expected actual
+'
+
+test_expect_success 'grep --only-matching -i' '
+       cat >expected <<-\EOF &&
+       hello_world:1:1:Hello
+       hello_world:2:1:HeLLo
+       hello_world:3:1:Hello
+       hello_world:4:1:HeLLo
+       EOF
+       git grep --only-matching --line-number --column \
+               -i hello hello_world >actual &&
+       test_cmp expected actual
+'
+
 cat >expected <<EOF
 <BOLD;GREEN>hello.c<RESET>
 4:int main(int argc, const <BLACK;BYELLOW>char<RESET> **argv)
-- 
2.17.0

Reply via email to