Merge branch 'tb/grep-column'

"git grep" learned the "--column" option that gives not just the
line number but the column number of the hit.

* tb/grep-column:
  contrib/git-jump/git-jump: jump to exact location
  grep.c: add configuration variables to show matched option
  builtin/grep.c: add '--column' option to 'git-grep(1)'
  grep.c: display column number of first match
  grep.[ch]: extend grep_opt to allow showing matched column
  grep.c: expose {,inverted} match column in match_line()
  Documentation/config.txt: camel-case lineNumber for consistency
This commit is contained in:
Junio C Hamano 2018-07-18 12:20:31 -07:00
commit d036d667b7
8 changed files with 226 additions and 34 deletions

View File

@ -1182,8 +1182,10 @@ color.grep.<slot>::
filename prefix (when not using `-h`)
`function`;;
function name lines (when using `-p`)
`linenumber`;;
`lineNumber`;;
line number prefix (when using `-n`)
`column`;;
column number prefix (when using `--column`)
`match`;;
matching text (same as setting `matchContext` and `matchSelected`)
`matchContext`;;
@ -1798,6 +1800,9 @@ gitweb.snapshot::
grep.lineNumber::
If set to true, enable `-n` option by default.
grep.column::
If set to true, enable the `--column` option by default.
grep.patternType::
Set the default matching behavior. Using a value of 'basic', 'extended',
'fixed', or 'perl' will enable the `--basic-regexp`, `--extended-regexp`,

View File

@ -13,7 +13,7 @@ SYNOPSIS
[-v | --invert-match] [-h|-H] [--full-name]
[-E | --extended-regexp] [-G | --basic-regexp]
[-P | --perl-regexp]
[-F | --fixed-strings] [-n | --line-number]
[-F | --fixed-strings] [-n | --line-number] [--column]
[-l | --files-with-matches] [-L | --files-without-match]
[(-O | --open-files-in-pager) [<pager>]]
[-z | --null]
@ -44,6 +44,9 @@ CONFIGURATION
grep.lineNumber::
If set to true, enable `-n` option by default.
grep.column::
If set to true, enable the `--column` option by default.
grep.patternType::
Set the default matching behavior. Using a value of 'basic', 'extended',
'fixed', or 'perl' will enable the `--basic-regexp`, `--extended-regexp`,
@ -169,6 +172,10 @@ providing this option will cause it to die.
--line-number::
Prefix the line number to matching lines.
--column::
Prefix the 1-indexed byte-offset of the first match from the start of the
matching line.
-l::
--files-with-matches::
--name-only::

View File

@ -828,6 +828,7 @@ int cmd_grep(int argc, const char **argv, const char *prefix)
GREP_PATTERN_TYPE_PCRE),
OPT_GROUP(""),
OPT_BOOL('n', "line-number", &opt.linenum, N_("show line numbers")),
OPT_BOOL(0, "column", &opt.columnnum, N_("show column number of first match")),
OPT_NEGBIT('h', NULL, &opt.pathname, N_("don't show filenames"), 1),
OPT_BIT('H', NULL, &opt.pathname, N_("show filenames"), 1),
OPT_NEGBIT(0, "full-name", &opt.relative,

View File

@ -25,6 +25,13 @@ git-jump will feed this to the editor:
foo.c:2: printf("hello word!\n");
-----------------------------------
Or, when running 'git jump grep', column numbers will also be emitted,
e.g. `git jump grep "hello"` would return:
-----------------------------------
foo.c:2:9: printf("hello word!\n");
-----------------------------------
Obviously this trivial case isn't that interesting; you could just open
`foo.c` yourself. But when you have many changes scattered across a
project, you can use the editor's support to "jump" from point to point.
@ -35,7 +42,8 @@ Git-jump can generate four types of interesting lists:
2. The beginning of any merge conflict markers.
3. Any grep matches.
3. Any grep matches, including the column of the first match on a
line.
4. Any whitespace errors detected by `git diff --check`.
@ -82,7 +90,7 @@ which does something similar to `git jump grep`. However, it is limited
to positioning the cursor to the correct line in only the first file,
leaving you to locate subsequent hits in that file or other files using
the editor or pager. By contrast, git-jump provides the editor with a
complete list of files and line numbers for each match.
complete list of files, lines, and a column number for each match.
Limitations

View File

@ -52,7 +52,7 @@ mode_merge() {
# editor shows them to us in the status bar.
mode_grep() {
cmd=$(git config jump.grepCmd)
test -n "$cmd" || cmd="git grep -n"
test -n "$cmd" || cmd="git grep -n --column"
$cmd "$@" |
perl -pe '
s/[ \t]+/ /g;

130
grep.c
View File

@ -20,6 +20,7 @@ static const char *color_grep_slots[] = {
[GREP_COLOR_FILENAME] = "filename",
[GREP_COLOR_FUNCTION] = "function",
[GREP_COLOR_LINENO] = "lineNumber",
[GREP_COLOR_COLUMNNO] = "column",
[GREP_COLOR_MATCH_CONTEXT] = "matchContext",
[GREP_COLOR_MATCH_SELECTED] = "matchSelected",
[GREP_COLOR_SELECTED] = "selected",
@ -59,6 +60,7 @@ void init_grep_defaults(void)
color_set(opt->colors[GREP_COLOR_FILENAME], "");
color_set(opt->colors[GREP_COLOR_FUNCTION], "");
color_set(opt->colors[GREP_COLOR_LINENO], "");
color_set(opt->colors[GREP_COLOR_COLUMNNO], "");
color_set(opt->colors[GREP_COLOR_MATCH_CONTEXT], GIT_COLOR_BOLD_RED);
color_set(opt->colors[GREP_COLOR_MATCH_SELECTED], GIT_COLOR_BOLD_RED);
color_set(opt->colors[GREP_COLOR_SELECTED], "");
@ -110,6 +112,10 @@ int grep_config(const char *var, const char *value, void *cb)
opt->linenum = git_config_bool(var, value);
return 0;
}
if (!strcmp(var, "grep.column")) {
opt->columnnum = git_config_bool(var, value);
return 0;
}
if (!strcmp(var, "grep.fullname")) {
opt->relative = !git_config_bool(var, value);
@ -157,6 +163,7 @@ void grep_init(struct grep_opt *opt, const char *prefix)
opt->extended_regexp_option = def->extended_regexp_option;
opt->pattern_type_option = def->pattern_type_option;
opt->linenum = def->linenum;
opt->columnnum = def->columnnum;
opt->max_depth = def->max_depth;
opt->pathname = def->pathname;
opt->relative = def->relative;
@ -1244,11 +1251,11 @@ static int match_one_pattern(struct grep_pat *p, char *bol, char *eol,
return hit;
}
static int match_expr_eval(struct grep_expr *x, char *bol, char *eol,
enum grep_context ctx, int collect_hits)
static int match_expr_eval(struct grep_opt *opt, struct grep_expr *x, char *bol,
char *eol, enum grep_context ctx, ssize_t *col,
ssize_t *icol, int collect_hits)
{
int h = 0;
regmatch_t match;
if (!x)
die("Not a valid grep expression");
@ -1257,25 +1264,52 @@ static int match_expr_eval(struct grep_expr *x, char *bol, char *eol,
h = 1;
break;
case GREP_NODE_ATOM:
h = match_one_pattern(x->u.atom, bol, eol, ctx, &match, 0);
{
regmatch_t tmp;
h = match_one_pattern(x->u.atom, bol, eol, ctx,
&tmp, 0);
if (h && (*col < 0 || tmp.rm_so < *col))
*col = tmp.rm_so;
}
break;
case GREP_NODE_NOT:
h = !match_expr_eval(x->u.unary, bol, eol, ctx, 0);
/*
* Upon visiting a GREP_NODE_NOT, col and icol become swapped.
*/
h = !match_expr_eval(opt, x->u.unary, bol, eol, ctx, icol, col,
0);
break;
case GREP_NODE_AND:
if (!match_expr_eval(x->u.binary.left, bol, eol, ctx, 0))
return 0;
h = match_expr_eval(x->u.binary.right, bol, eol, ctx, 0);
h = match_expr_eval(opt, x->u.binary.left, bol, eol, ctx, col,
icol, 0);
if (h || opt->columnnum) {
/*
* Don't short-circuit AND when given --column, since a
* NOT earlier in the tree may turn this into an OR. In
* this case, see the below comment.
*/
h &= match_expr_eval(opt, x->u.binary.right, bol, eol,
ctx, col, icol, 0);
}
break;
case GREP_NODE_OR:
if (!collect_hits)
return (match_expr_eval(x->u.binary.left,
bol, eol, ctx, 0) ||
match_expr_eval(x->u.binary.right,
bol, eol, ctx, 0));
h = match_expr_eval(x->u.binary.left, bol, eol, ctx, 0);
if (!(collect_hits || opt->columnnum)) {
/*
* Don't short-circuit OR when given --column (or
* collecting hits) to ensure we don't skip a later
* child that would produce an earlier match.
*/
return (match_expr_eval(opt, x->u.binary.left, bol, eol,
ctx, col, icol, 0) ||
match_expr_eval(opt, x->u.binary.right, bol,
eol, ctx, col, icol, 0));
}
h = match_expr_eval(opt, x->u.binary.left, bol, eol, ctx, col,
icol, 0);
if (collect_hits)
x->u.binary.left->hit |= h;
h |= match_expr_eval(x->u.binary.right, bol, eol, ctx, 1);
h |= match_expr_eval(opt, x->u.binary.right, bol, eol, ctx, col,
icol, collect_hits);
break;
default:
die("Unexpected node type (internal error) %d", x->node);
@ -1286,27 +1320,43 @@ static int match_expr_eval(struct grep_expr *x, char *bol, char *eol,
}
static int match_expr(struct grep_opt *opt, char *bol, char *eol,
enum grep_context ctx, int collect_hits)
enum grep_context ctx, ssize_t *col,
ssize_t *icol, int collect_hits)
{
struct grep_expr *x = opt->pattern_expression;
return match_expr_eval(x, bol, eol, ctx, collect_hits);
return match_expr_eval(opt, x, bol, eol, ctx, col, icol, collect_hits);
}
static int match_line(struct grep_opt *opt, char *bol, char *eol,
ssize_t *col, ssize_t *icol,
enum grep_context ctx, int collect_hits)
{
struct grep_pat *p;
regmatch_t match;
int hit = 0;
if (opt->extended)
return match_expr(opt, bol, eol, ctx, collect_hits);
return match_expr(opt, bol, eol, ctx, col, icol,
collect_hits);
/* we do not call with collect_hits without being extended */
for (p = opt->pattern_list; p; p = p->next) {
if (match_one_pattern(p, bol, eol, ctx, &match, 0))
return 1;
regmatch_t tmp;
if (match_one_pattern(p, bol, eol, ctx, &tmp, 0)) {
hit |= 1;
if (!opt->columnnum) {
/*
* Without --column, any single match on a line
* is enough to know that it needs to be
* printed. With --column, scan _all_ patterns
* to find the earliest.
*/
break;
}
return 0;
if (*col < 0 || tmp.rm_so < *col)
*col = tmp.rm_so;
}
}
return hit;
}
static int match_next_pattern(struct grep_pat *p, char *bol, char *eol,
@ -1355,7 +1405,7 @@ static int next_match(struct grep_opt *opt, char *bol, char *eol,
}
static void show_line(struct grep_opt *opt, char *bol, char *eol,
const char *name, unsigned lno, char sign)
const char *name, unsigned lno, ssize_t cno, char sign)
{
int rest = eol - bol;
const char *match_color, *line_color = NULL;
@ -1390,6 +1440,17 @@ static void show_line(struct grep_opt *opt, char *bol, char *eol,
output_color(opt, buf, strlen(buf), opt->colors[GREP_COLOR_LINENO]);
output_sep(opt, sign);
}
/*
* Treat 'cno' as the 1-indexed offset from the start of a non-context
* line to its first match. Otherwise, 'cno' is 0 indicating that we are
* being called with a context line.
*/
if (opt->columnnum && cno) {
char buf[32];
xsnprintf(buf, sizeof(buf), "%"PRIuMAX, (uintmax_t)cno);
output_color(opt, buf, strlen(buf), opt->colors[GREP_COLOR_COLUMNNO]);
output_sep(opt, sign);
}
if (opt->color) {
regmatch_t match;
enum grep_context ctx = GREP_CONTEXT_BODY;
@ -1495,7 +1556,7 @@ static void show_funcname_line(struct grep_opt *opt, struct grep_source *gs,
break;
if (match_funcname(opt, gs, bol, eol)) {
show_line(opt, bol, eol, gs->name, lno, '=');
show_line(opt, bol, eol, gs->name, lno, 0, '=');
break;
}
}
@ -1560,7 +1621,7 @@ static void show_pre_context(struct grep_opt *opt, struct grep_source *gs,
while (*eol != '\n')
eol++;
show_line(opt, bol, eol, gs->name, cur, sign);
show_line(opt, bol, eol, gs->name, cur, 0, sign);
bol = eol + 1;
cur++;
}
@ -1759,6 +1820,8 @@ static int grep_source_1(struct grep_opt *opt, struct grep_source *gs, int colle
while (left) {
char *eol, ch;
int hit;
ssize_t cno;
ssize_t col = -1, icol = -1;
/*
* look_ahead() skips quickly to the line that possibly
@ -1782,7 +1845,7 @@ static int grep_source_1(struct grep_opt *opt, struct grep_source *gs, int colle
if ((ctx == GREP_CONTEXT_HEAD) && (eol == bol))
ctx = GREP_CONTEXT_BODY;
hit = match_line(opt, bol, eol, ctx, collect_hits);
hit = match_line(opt, bol, eol, &col, &icol, ctx, collect_hits);
*eol = ch;
if (collect_hits)
@ -1823,7 +1886,18 @@ static int grep_source_1(struct grep_opt *opt, struct grep_source *gs, int colle
show_pre_context(opt, gs, bol, eol, lno);
else if (opt->funcname)
show_funcname_line(opt, gs, bol, lno);
show_line(opt, bol, eol, gs->name, lno, ':');
cno = opt->invert ? icol : col;
if (cno < 0) {
/*
* A negative cno indicates that there was no
* match on the line. We are thus inverted and
* being asked to show all lines that _don't_
* match a given expression. Therefore, set cno
* to 0 to suggest the whole line matches.
*/
cno = 0;
}
show_line(opt, bol, eol, gs->name, lno, cno + 1, ':');
last_hit = lno;
if (opt->funcbody)
show_function = 1;
@ -1852,7 +1926,7 @@ static int grep_source_1(struct grep_opt *opt, struct grep_source *gs, int colle
/* If the last hit is within the post context,
* we need to show this line.
*/
show_line(opt, bol, eol, gs->name, lno, '-');
show_line(opt, bol, eol, gs->name, lno, col + 1, '-');
}
next_line:

2
grep.h
View File

@ -67,6 +67,7 @@ enum grep_color {
GREP_COLOR_FILENAME,
GREP_COLOR_FUNCTION,
GREP_COLOR_LINENO,
GREP_COLOR_COLUMNNO,
GREP_COLOR_MATCH_CONTEXT,
GREP_COLOR_MATCH_SELECTED,
GREP_COLOR_SELECTED,
@ -139,6 +140,7 @@ struct grep_opt {
int prefix_length;
regex_t regexp;
int linenum;
int columnnum;
int invert;
int ignore_case;
int status_only;

View File

@ -99,6 +99,101 @@ do
test_cmp expected actual
'
test_expect_success "grep -w $L (with --column)" '
{
echo ${HC}file:5:foo mmap bar
echo ${HC}file:14:foo_mmap bar mmap
echo ${HC}file:5:foo mmap bar_mmap
echo ${HC}file:14:foo_mmap bar mmap baz
} >expected &&
git grep --column -w -e mmap $H >actual &&
test_cmp expected actual
'
test_expect_success "grep -w $L (with --column, extended OR)" '
{
echo ${HC}file:14:foo_mmap bar mmap
echo ${HC}file:19:foo_mmap bar mmap baz
} >expected &&
git grep --column -w -e mmap$ --or -e baz $H >actual &&
test_cmp expected actual
'
test_expect_success "grep -w $L (with --column, --invert)" '
{
echo ${HC}file:1:foo mmap bar
echo ${HC}file:1:foo_mmap bar
echo ${HC}file:1:foo_mmap bar mmap
echo ${HC}file:1:foo mmap bar_mmap
} >expected &&
git grep --column --invert -w -e baz $H -- file >actual &&
test_cmp expected actual
'
test_expect_success "grep $L (with --column, --invert, extended OR)" '
{
echo ${HC}hello_world:6:HeLLo_world
} >expected &&
git grep --column --invert -e ll --or --not -e _ $H -- hello_world \
>actual &&
test_cmp expected actual
'
test_expect_success "grep $L (with --column, --invert, extended AND)" '
{
echo ${HC}hello_world:3:Hello world
echo ${HC}hello_world:3:Hello_world
echo ${HC}hello_world:6:HeLLo_world
} >expected &&
git grep --column --invert --not -e _ --and --not -e ll $H -- hello_world \
>actual &&
test_cmp expected actual
'
test_expect_success "grep $L (with --column, double-negation)" '
{
echo ${HC}file:1:foo_mmap bar mmap baz
} >expected &&
git grep --column --not \( --not -e foo --or --not -e baz \) $H -- file \
>actual &&
test_cmp expected actual
'
test_expect_success "grep -w $L (with --column, -C)" '
{
echo ${HC}file:5:foo mmap bar
echo ${HC}file-foo_mmap bar
echo ${HC}file:14:foo_mmap bar mmap
echo ${HC}file:5:foo mmap bar_mmap
echo ${HC}file:14:foo_mmap bar mmap baz
} >expected &&
git grep --column -w -C1 -e mmap $H >actual &&
test_cmp expected actual
'
test_expect_success "grep -w $L (with --line-number, --column)" '
{
echo ${HC}file:1:5:foo mmap bar
echo ${HC}file:3:14:foo_mmap bar mmap
echo ${HC}file:4:5:foo mmap bar_mmap
echo ${HC}file:5:14:foo_mmap bar mmap baz
} >expected &&
git grep -n --column -w -e mmap $H >actual &&
test_cmp expected actual
'
test_expect_success "grep -w $L (with non-extended patterns, --column)" '
{
echo ${HC}file:5:foo mmap bar
echo ${HC}file:10:foo_mmap bar
echo ${HC}file:10:foo_mmap bar mmap
echo ${HC}file:5:foo mmap bar_mmap
echo ${HC}file:10:foo_mmap bar mmap baz
} >expected &&
git grep --column -w -e bar -e mmap $H >actual &&
test_cmp expected actual
'
test_expect_success "grep -w $L" '
{
echo ${HC}file:1:foo mmap bar