git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v2 2/2] builtin/grep.c: teach '-o', '--only-matching' to 'git-grep'

From
Taylor Blau <me@ttaylorr.com>
Date
May 12, 2018, 03:21 UTC
Message-ID
<8258dac743a137196a6c43b2317248c5c079c115.1526094978.git.me@ttaylorr.com>
In-Reply-To
<cover.1526094978.git.me@ttaylorr.com>

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 <me@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
Previous: Taylor Blau
Message 14 of 14 in “builtin/grep.c: teach '-o', '--only-matching'”
  1. 0/2 builtin/grep.c: teach '-o', '--only-matching'Taylor Blau, May 5, 2018
  2. 1/2 grep.c: extract show_line_header()Taylor Blau, May 5, 2018
  3. Eric SunshineMay 5, 2018
  4. Taylor BlauMay 8, 2018
  5. 2/2 builtin/grep.c: teach '-o', '--only-matching' to 'git-grep'Taylor Blau, May 5, 2018
  6. Ævar Arnfjörð BjarmasonMay 5, 2018
  7. Jeff KingMay 8, 2018
  8. Taylor BlauMay 10, 2018
  9. Jeff KingMay 10, 2018
  10. Eric SunshineMay 5, 2018
  11. Taylor BlauMay 8, 2018
  12. 0/2 builtin/grep.c: teach '-o', '--only-matching'Taylor Blau, May 12, 2018
  13. 1/2 grep.c: extract show_line_header()Taylor Blau, May 12, 2018
  14. 2/2 builtin/grep.c: teach '-o', '--only-matching' to 'git-grep'Taylor Blau, May 12, 2018

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.