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@xxxxxxxxxxxx> --- 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