I want to grep a search pattern but only succeed (and output the matching line) if there is only one unique match. If two lines match, grep should fail or output nothing.
-
9What have you tried and where are you stuck? – Kamil Maciorowski Jul 15 '22 at 11:37
2 Answers
You can't do this with grep
, but you can simply count the matches. I don't know what shell, what grep
or what operating system you are using, but here's an example of a bash function that can do that:
maxOne() (
pattern="$1"
file="$2"
IFS=$'\n'
set -f
results=( $(grep -m2 -- "$pattern" "$file") )
if [ "${#results[@]}" -eq 1 ]; then
printf -- '%s\n' "${results[@]}"
return 0
else
return 1
fi
)
Add those lines to your ~/.bashrc
or just paste them into a terminal with a running bash session, and you can then do:
maxOne foo file
To search for foo
in file
. Note that the -m
option (maximum results) which is used here for efficiency to make grep
exit after two matches, isn't supported by all versions of grep
so if it gives you an error, just remove it. It isn't needed, it just speed things up.
Important: this will not work for multi-line search strings which you can use with grep -z
if your grep
supports that. If you need to be able to handle multi-line search patterns, you will need a different approach. Also, this will not work with patterns that match empty lines (e.g. grep '^$' file
). Stéphane's solution will handle empty lines, so that would be a better option if this is an issue. His will also work on multiple files, unlike mine, which is a nice perk.

- 242,166
-
(The question said "only succeed if there is only one unique match", which I took to mean that zero matches should not succeed. Anyway,
printf -- '%s\n' "${results[@]}"
would still print one empty line if the array was empty. Not because the array expansion would conjure up an empty element, but becauseprintf
prints the format string at least once.) – ilkkachu Jul 15 '22 at 12:35 -
aaand
set -f
has a different meaning in zsh (but isn't really necessary). Not sure if it's worth making the function usable in both with that issue... – ilkkachu Jul 15 '22 at 12:43 -
1@ilkkachu
set -o noglob
works the same in zsh and bash (and is more legible IMO) – Stéphane Chazelas Jul 15 '22 at 12:58 -
1Beware
array=( $(grep...) )
would remove empty lines from the output ofgrep
, so you can't use that if the pattern may match empty lines. Withbash
, you can usereadarray -t array < <(grep...)
instead which avoids having to mess withIFS
andnoglob
. See also thef
parameter expansion flag inzsh
. – Stéphane Chazelas Jul 15 '22 at 14:07 -
@ilkkachu fair point about
printf
but the rest of your edits seem to only have made it worse: I want thefunction ()
since I don't want this to be run on shells that don't supportfunction
. As you said,set -f
doesn't do the same thing in zsh, so why add it? Where do you want to disable globbing? – terdon Jul 15 '22 at 14:30 -
@StéphaneChazelas I was always thinking that this would not handle patterns with newlines (but I forgot to make that explicit). Is there any reason to mess with IFS if I do not need to handle newlines? – terdon Jul 15 '22 at 14:34
-
@terdon, well, I expect you'd want to disable word-splitting and globbing when splitting the output of the
$(...)
to the array. Consider a file where the lines have multiple words or consist of e.g. a lone asterisk.echo hello world > test.txt; maxOne hello test.txt
and it fails sincehello
andworld
produce two elements in the array. Orecho '*' > test.txt; maxOne . test.txt
, where the glob gets expanded probably giving more than one array element. – ilkkachu Jul 15 '22 at 15:56 -
@terdon, as for
function maxOne()
, that's not supported in ksh (wherefunction foo
andfoo()
are both supported but subtly different). The rest of the array stuff required would work in ksh, though (and Bash's arrays are borrowed from ksh anyway). So I'm not sure why you'd want to make that part an arbitrary filter. (It looks to me that arrays are the feature actually needed here, and a shell that doesn't support (ksh) arrays would likely croak at"${#results[@]}"
or one of the others anyway.) But sure, it's your answer. – ilkkachu Jul 15 '22 at 15:58 -
@ilkkachu ah! Of course, in the array. Absolutely yes, thanks. I'll add
set -o noglob
as Stéphane suggested. As forfunction
, if removing it makes it work inksh
as well, then thank you again and I'll do that. I had thought it was the POSIX shells likesh
anddash
that would choke onfunction
and since I didn't want this to work for them anyway, I saw no point. I learned a few things today, thanks! – terdon Jul 15 '22 at 16:03 -
@terdon, you need to change
IFS
too, since the default would split on any whitespace, splitting words within a line, not just the lines from each other. And then there's the issue that those changes affect global state, so you'd need to resetIFS
and thenoglob
flag at the end to avoid messing up other parts of the script... So easiest to wrap the whole function in( )
instead of{ }
to run it in a subshell. Or uselocal - IFS;
in Bash (the-
makesnoglob
and other flags local too), butlocal
is where ksh is different and I'm not sure it can localize the flags... – ilkkachu Jul 15 '22 at 16:19 -
@terdon, Um, yeah. I though about writing a longer comment at first, before (or instead of) editing, but, I guess, I thought it was an obvious enough word-splitting issue anyway and wanted to spare everyone from the verbose explanation... Sorry. – ilkkachu Jul 15 '22 at 16:30
-
-
Adding the
-m2
option togrep
(GNU-specific though) would avoid it looking for occurrences past the second like in my answer to make it more efficient. – Stéphane Chazelas Jul 16 '22 at 11:59 -
You could do with:
unique_egrep() (
export ERE="$1"; shift
exec gawk -e '
BEGIN {ret = 1}
BEGINFILE {n = 0}
$0 ~ ENVIRON["ERE"] {if (n++) nextfile; found = $0}
ENDFILE {if (n == 1) {print FILENAME":"found; ret = 0}}
END {exit ret}' -E /dev/null "$@"
)
And then unique_egrep pattern *.txt
for instance.
Here using the -e 'code' -E /dev/null
(in place of 'code'
) trick to be able to process arbitrary file paths.
All of -e
, -E
, BEGINFILE
, ENDFILE
and nextfile
are GNU extensions (though nextfile
is now found in many other implementations as well).

- 544,893
-
This answer looks very good, but could you briefly explain what is the purpose of
-E /dev/null
in the last line of thegawk
script? – user000001 Jul 16 '22 at 09:45 -
2