grep a pattern and output non-matching part of line

Viewed 14830

I know it is possible to invert grep output with the -v flag. Is there a way to only output the non-matching part of the matched line? I ask because I would like to use the return code of grep (which sed won't have). Here's sort of what I've got:

tags=$(grep "^$PAT" >/dev/null 2>&1)
[ "$?" -eq 0 ] && echo $tags 
4 Answers

I am going to answer the title of the question directly instead of considering the detail of the question itself:

"grep a pattern and output non-matching part of line"

The title to this question is important to me because the pattern I am searching for contains characters that sed will assign special meaning to. I want to use grep because I can use -F or --fixed-strings to cause grep to interpret the pattern literally. Unfortunately, sed has no literal option, but both grep and bash have the ability to interpret patterns without considering any special characters.

Note: In my opinion, trying to backslash or escape special characters in a pattern appears complex in code and is unreliable because it is difficult to test. Using tools which are designed to search for literal text leaves me with a comfortable 'that will work' feeling without considering POSIX.

I used both grep and bash to produce the result because bash is slow and my use of fast grep creates a small output from a large input. This code searches for the literal twice, once during grep to quickly extract matching lines and once during =~ to remove the match itself from each line.

    while IFS= read -r || [[ -n "$RESULT" ]]; do
        if [[ "$REPLY" =~ (.*)("$LITERAL_PATTERN")(.*) ]]; then
            printf '%s\n' "${BASH_REMATCH[1]}${BASH_REMATCH[3]}"
        else
            printf "NOT-REFOUND" # should never happen
            exit 1
        fi
    done < <(grep -F "$LITERAL_PATTERN" < "$INPUT_FILE")

Explanation:

IFS= Reassigning the input field separator is a special prefix for a read statement. Assigning IFS to the empty string causes read to accept each line with all spaces and tabs literally until end of line (assuming IFS is default space-tab-newline).

-r Tells read to accept backslashes in the input stream literally instead of considering them as the start of an escape sequence.

$REPLY Is created by read to store characters from the input stream. The newline at the end of each line will NOT be in $REPLY.

|| [[ -n "$REPLY" ]] The logical or causes the while loop to accept input which is not newline terminated. This does not need to exist because grep always provides a trailing newline for every match. But, I habitually use this in my read loops because without it, characters between the last newline and the end of file will be ignored because that causes read to fail even though content is successfully read.

=~ (.*)("$LITERAL_PATTERN")(.*) ]] Is a standard bash regex test, but anything in quotes in taken as a literal. If I wanted =~ to consider the regex characters in contained in $PATTERN, then I would need to eliminate the double quotes.

"${BASH_REMATCH[@]}" Is created by [[ =~ ]] where [0] is the entire match and [N] is the contents of the match in the Nth set of parentheses.

Note: I do not like to reassign stdin to a while loop because it is easy to error and difficult to see what is happening later. I usually create a function for this type of operation which acts typically and expects file_name parameters or reassignment of stdin during the call.

Related