Skip to content

Commit d99ebf0

Browse files
committed
Split grep arguments in a way that does not requires to add /dev/null.
In order to (almost) always show the name of the file without relying on "-H" option of GNU grep, we used to add /dev/null to the argument list unless we are doing -l or -L. This caused "/dev/null:0" to show up when -c is given in the output. It is not enough to add -c to the set of options we do not pass /dev/null for. When we have too many files, we invoke grep multiple times and we need to avoid giving a widow filename to the last invocation -- otherwise we will not see the name. This keeps two filenames when the argv[] buffer is about to overflow and we have not finished iterating over the index, so that the last round will always have at least two paths to work with (and not require /dev/null). An obvious and the only exception is when there is only 1 file that is given to the underlying grep, and in that case we avoid passing /dev/null and let the external "grep -c" report only the number of matches. Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 7c8b5ea commit d99ebf0

File tree

2 files changed

+80
-14
lines changed

2 files changed

+80
-14
lines changed

builtin-grep.c

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,78 @@ static int exec_grep(int argc, const char **argv)
187187
else die("maximum number of args exceeded"); \
188188
} while (0)
189189

190+
/*
191+
* If you send a singleton filename to grep, it does not give
192+
* the name of the file. GNU grep has "-H" but we would want
193+
* that behaviour in a portable way.
194+
*
195+
* So we keep two pathnames in argv buffer unsent to grep in
196+
* the main loop if we need to do more than one grep.
197+
*/
198+
static int flush_grep(struct grep_opt *opt,
199+
int argc, int arg0, const char **argv, int *kept)
200+
{
201+
int status;
202+
int count = argc - arg0;
203+
const char *kept_0 = NULL;
204+
205+
if (count <= 2) {
206+
/*
207+
* Because we keep at least 2 paths in the call from
208+
* the main loop (i.e. kept != NULL), and MAXARGS is
209+
* far greater than 2, this usually is a call to
210+
* conclude the grep. However, the user could attempt
211+
* to overflow the argv buffer by giving too many
212+
* options to leave very small number of real
213+
* arguments even for the call in the main loop.
214+
*/
215+
if (kept)
216+
die("insanely many options to grep");
217+
218+
/*
219+
* If we have two or more paths, we do not have to do
220+
* anything special, but we need to push /dev/null to
221+
* get "-H" behaviour of GNU grep portably but when we
222+
* are not doing "-l" nor "-L" nor "-c".
223+
*/
224+
if (count == 1 &&
225+
!opt->name_only &&
226+
!opt->unmatch_name_only &&
227+
!opt->count) {
228+
argv[argc++] = "/dev/null";
229+
argv[argc] = NULL;
230+
}
231+
}
232+
233+
else if (kept) {
234+
/*
235+
* Called because we found many paths and haven't finished
236+
* iterating over the cache yet. We keep two paths
237+
* for the concluding call. argv[argc-2] and argv[argc-1]
238+
* has the last two paths, so save the first one away,
239+
* replace it with NULL while sending the list to grep,
240+
* and recover them after we are done.
241+
*/
242+
*kept = 2;
243+
kept_0 = argv[argc-2];
244+
argv[argc-2] = NULL;
245+
argc -= 2;
246+
}
247+
248+
status = exec_grep(argc, argv);
249+
250+
if (kept_0) {
251+
/*
252+
* Then recover them. Now the last arg is beyond the
253+
* terminating NULL which is at argc, and the second
254+
* from the last is what we saved away in kept_0
255+
*/
256+
argv[arg0++] = kept_0;
257+
argv[arg0] = argv[argc+1];
258+
}
259+
return status;
260+
}
261+
190262
static int external_grep(struct grep_opt *opt, const char **paths, int cached)
191263
{
192264
int i, nr, argc, hit, len, status;
@@ -253,22 +325,12 @@ static int external_grep(struct grep_opt *opt, const char **paths, int cached)
253325
push_arg(p->pattern);
254326
}
255327

256-
/*
257-
* To make sure we get the header printed out when we want it,
258-
* add /dev/null to the paths to grep. This is unnecessary
259-
* (and wrong) with "-l" or "-L", which always print out the
260-
* name anyway.
261-
*
262-
* GNU grep has "-H", but this is portable.
263-
*/
264-
if (!opt->name_only && !opt->unmatch_name_only)
265-
push_arg("/dev/null");
266-
267328
hit = 0;
268329
argc = nr;
269330
for (i = 0; i < active_nr; i++) {
270331
struct cache_entry *ce = active_cache[i];
271332
char *name;
333+
int kept;
272334
if (!S_ISREG(ntohl(ce->ce_mode)))
273335
continue;
274336
if (!pathspec_matches(paths, ce->name))
@@ -283,10 +345,10 @@ static int external_grep(struct grep_opt *opt, const char **paths, int cached)
283345
argv[argc++] = name;
284346
if (argc < MAXARGS && !ce_stage(ce))
285347
continue;
286-
status = exec_grep(argc, argv);
348+
status = flush_grep(opt, argc, nr, argv, &kept);
287349
if (0 < status)
288350
hit = 1;
289-
argc = nr;
351+
argc = nr + kept;
290352
if (ce_stage(ce)) {
291353
do {
292354
i++;
@@ -296,7 +358,7 @@ static int external_grep(struct grep_opt *opt, const char **paths, int cached)
296358
}
297359
}
298360
if (argc > nr) {
299-
status = exec_grep(argc, argv);
361+
status = flush_grep(opt, argc, nr, argv, NULL);
300362
if (0 < status)
301363
hit = 1;
302364
}

t/t7002-grep.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ do
107107
diff expected actual
108108
'
109109

110+
test_expect_failure "grep -c $L (no /dev/null)" '
111+
git grep -c test $H | grep -q "/dev/null"
112+
'
113+
110114
done
111115

112116
test_done

0 commit comments

Comments
 (0)