grep.c: teach 'git grep --only-matching'

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>

Taylor Blau committed Jul 9, 2018 at 15:33 UTC 9d8db06eb4a5d5577db559a0059a377914c1ce5a
5 files changed +63 -17
Documentation/git-grep.txt
+6 -1
@@ -17,7 +17,7 @@ SYNOPSIS
17 [-l | --files-with-matches] [-L | --files-without-match]
18 [(-O | --open-files-in-pager) [<pager>]]
19 [-z | --null]
20 - [-c | --count] [--all-match] [-q | --quiet]
20 + [ -o | --only-matching ] [-c | --count] [--all-match] [-q | --quiet]
21 [--max-depth <depth>]
22 [--color[=<when>] | --no-color]
23 [--break] [--heading] [-p | --show-function]
@@ -201,6 +201,11 @@ providing this option will cause it to die.
201 Output \0 instead of the character that normally follows a
202 file name.
203
204 +-o::
205 +--only-matching::
206 + Print only the matched (non-empty) parts of a matching line, with each such
207 + part on a separate output line.
208 +
209 -c::
210 --count::
211 Instead of showing every matched line, show the number of
builtin/grep.c
+6
@@ -843,6 +843,8 @@ int cmd_grep(int argc, const char **argv, const char *prefix)
843 OPT_BOOL_F('z', "null", &opt.null_following_name,
844 N_("print NUL after filenames"),
845 PARSE_OPT_NOCOMPLETE),
846 + OPT_BOOL('o', "only-matching", &opt.only_matching,
847 + N_("show only matching parts of a line")),
848 OPT_BOOL('c', "count", &opt.count,
849 N_("show the number of matches instead of matching lines")),
850 OPT__COLOR(&opt.color, N_("highlight matches")),
@@ -962,6 +964,10 @@ int cmd_grep(int argc, const char **argv, const char *prefix)
964 if (!opt.pattern_list)
965 die(_("no pattern given."));
966
967 + /* --only-matching has no effect with --invert. */
968 + if (opt.invert)
969 + opt.only_matching = 0;
970 +
971 /*
972 * We have to find "--" in a separate pass, because its presence
973 * influences how we will parse arguments that come before it.
grep.c
+35 -16
@@ -51,6 +51,7 @@ void init_grep_defaults(void)
51 color_set(opt->color_match_selected, GIT_COLOR_BOLD_RED);
52 color_set(opt->color_selected, "");
53 color_set(opt->color_sep, GIT_COLOR_CYAN);
54 + opt->only_matching = 0;
55 opt->color = -1;
56 opt->output = std_output;
57 }
@@ -158,6 +159,7 @@ void grep_init(struct grep_opt *opt, const char *prefix)
159 opt->pattern_tail = &opt->pattern_list;
160 opt->header_tail = &opt->header_list;
161
162 + opt->only_matching = def->only_matching;
163 opt->color = def->color;
164 opt->extended_regexp_option = def->extended_regexp_option;
165 opt->pattern_type_option = def->pattern_type_option;
@@ -1446,7 +1448,8 @@ static void show_line(struct grep_opt *opt, char *bol, char *eol,
1448 const char *name, unsigned lno, ssize_t cno, char sign)
1449 {
1450 int rest = eol - bol;
1449 - const char *match_color, *line_color = NULL;
1451 + const char *match_color = NULL;
1452 + const char *line_color = NULL;
1453
1454 if (opt->file_break && opt->last_shown == 0) {
1455 if (opt->show_hunk_mark)
@@ -1462,39 +1465,55 @@ static void show_line(struct grep_opt *opt, char *bol, char *eol,
1465 opt->output(opt, "\n", 1);
1466 }
1467 }
1465 - show_line_header(opt, name, lno, cno, sign);
1466 - if (opt->color) {
1468 + if (!opt->only_matching) {
1469 + /*
1470 + * In case the line we're being called with contains more than
1471 + * one match, leave printing each header to the loop below.
1472 + */
1473 + show_line_header(opt, name, lno, cno, sign);
1474 + }
1475 + if (opt->color || opt->only_matching) {
1476 regmatch_t match;
1477 enum grep_context ctx = GREP_CONTEXT_BODY;
1478 int ch = *eol;
1479 int eflags = 0;
1480
1472 - if (sign == ':')
1473 - match_color = opt->color_match_selected;
1474 - else
1475 - match_color = opt->color_match_context;
1476 - if (sign == ':')
1477 - line_color = opt->color_selected;
1478 - else if (sign == '-')
1479 - line_color = opt->color_context;
1480 - else if (sign == '=')
1481 - line_color = opt->color_function;
1481 + if (opt->color) {
1482 + if (sign == ':')
1483 + match_color = opt->color_match_selected;
1484 + else
1485 + match_color = opt->color_match_context;
1486 + if (sign == ':')
1487 + line_color = opt->color_selected;
1488 + else if (sign == '-')
1489 + line_color = opt->color_context;
1490 + else if (sign == '=')
1491 + line_color = opt->color_function;
1492 + }
1493 *eol = '\0';
1494 while (next_match(opt, bol, eol, ctx, &match, eflags)) {
1495 if (match.rm_so == match.rm_eo)
1496 break;
1497
1487 - output_color(opt, bol, match.rm_so, line_color);
1498 + if (opt->only_matching)
1499 + show_line_header(opt, name, lno, cno, sign);
1500 + else
1501 + output_color(opt, bol, match.rm_so, line_color);
1502 output_color(opt, bol + match.rm_so,
1503 match.rm_eo - match.rm_so, match_color);
1504 + if (opt->only_matching)
1505 + opt->output(opt, "\n", 1);
1506 bol += match.rm_eo;
1507 + cno += match.rm_eo;
1508 rest -= match.rm_eo;
1509 eflags = REG_NOTBOL;
1510 }
1511 *eol = ch;
1512 }
1496 - output_color(opt, bol, rest, line_color);
1497 - opt->output(opt, "\n", 1);
1513 + if (!opt->only_matching) {
1514 + output_color(opt, bol, rest, line_color);
1515 + opt->output(opt, "\n", 1);
1516 + }
1517 }
1518
1519 #ifndef NO_PTHREADS
grep.h
+1
@@ -150,6 +150,7 @@ struct grep_opt {
150 int relative;
151 int pathname;
152 int null_following_name;
153 + int only_matching;
154 int color;
155 int max_depth;
156 int funcname;
t/t7810-grep.sh
+15
@@ -262,6 +262,21 @@ do
262 fi
263 '
264
265 + test_expect_success "grep $L (with --column, --only-matching)" '
266 + {
267 + echo ${HC}file:1:5:mmap
268 + echo ${HC}file:2:5:mmap
269 + echo ${HC}file:3:5:mmap
270 + echo ${HC}file:3:13:mmap
271 + echo ${HC}file:4:5:mmap
272 + echo ${HC}file:4:13:mmap
273 + echo ${HC}file:5:5:mmap
274 + echo ${HC}file:5:13:mmap
275 + } >expected &&
276 + git grep --column -n -o -e mmap $H >actual &&
277 + test_cmp expected actual
278 + '
279 +
280 test_expect_success "grep $L (t-1)" '
281 echo "${HC}t/t:1:test" >expected &&
282 git grep -n -e test $H >actual &&