Is it possible to use sed to replace last, last 2nd, last 3rd, last 4th (and so on) match (occurrence)?

Viewed 121

I'd like to use reasoning similar to the approach below, which in this case uses a number (starting at 1) to replace the nth occurrence, but we want something in reverse order, that is, last, last 2nd, last 3rd, last 4th and so on.

QUESTION: Is it possible to use sed to substitute last, last 2nd, last 3rd, last 4th (and so on) match (occurrence)? If yes, how?

NOTE: Any approach that converts these numeric values to something specific to sed (a bash function, for example) is also acceptable. Using sed in conjunction with some command commonly found on Linux is also an acceptable answer. However, it is necessary that we can also use sed with files by adding -i.

EXAMPLE

sed -z 's/<TARGET>/<REPLACE>/<NTH_MATCH>'

EXAMPLE USE I

read -r -d '' FILE_CONTENT << 'HEREDOC'
BEGIN
askdjisoadjsadmsadjnasjndnsakjn

jkjkljkljklkljlkjkljl
jkljkljkljkklj
hjkhjkhkjh

jkjkljkljklkljlkjkljl
jkljkljkljkklj
hjkhjkhkjh

jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
END
HEREDOC
SOURCE="${FILE_CONTENT:6:-3}"
echo "${SOURCE}" | sed -z 's/jkjkljkljklkljlkjkljl\njkljkljkljkklj\nhjkhjkhkjh/hbeuhyewuhdfjh\nfdsjisfdjiusdfijuhsfiuhsfdihusfdjhiusfdjkh\nkjhsfdjkfjkhsfdjknh/2'

EXAMPLE USE II

read -r -d '' FILE_CONTENT << 'HEREDOC'
BEGIN
askdjisoadjsadmsadjnasjndnsakjn

jkjkljkljklkljlkjkljl
jkljkljkljkklj
hjkhjkhkjh

jkjkljkljklkljlkjkljl
jkljkljkljkklj
hjkhjkhkjh

jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
END
HEREDOC
SOURCE="${FILE_CONTENT:6:-3}"
echo "${SOURCE}" | sed -z 's/jdsjhfbjdhs/IReplacedTheLast4th/4'

**TIP:** This approach can be used to replace the last match (occurrence) -> `sed -z 's/\(.*\)<TARGET>/\1<REPLACE>/'`.

**Thanks!** 
2 Answers

This might work for you (GNU sed and some bash):

cat <<\! > repStr
this stanza
is in
lowercase

!

cat repStr repStr repStr repStr > sourceFile

tac repStr |
sed -z 's#.*#s/&/\\U&/3#;s/\n/\\n/g' |
sed -e 'H;1h;$!d;x
        s/.*/echo "&"|tac/e' -f - -e 's/.*/echo "&"|tac/e' sourceFile

In essence, reverse the replacement string and source file and use the existing properties of sed to replace string(s) then reverse the source file back to normal. The example above replaces(uppercases) the third last occurrence of repStr.

The final sed command, slurps the sourceFile into the pattern space and is reversed in situ, the prepared (already reversed) sed substitution command is applied and then the pattern space is reversed back to normal.

An alternative:

sed -Ez ':a;s/^([^\o000]*)(this stanza\nis in\nlowercase\n.*)[\o000]?/x\1\o000\2/
         tb;:b;s/^x{2}//;Ta
         s/\o000(this stanza\nis in\nlowercase\n)/\U\1/' sourceFile

This uses the -z option to slurp the file into memory and then uses the null character (\o000) as a delimiter to point to the beginning of the string to be replaced (amended).

N.B. The string and its number of occurrence to be replaced (amended) from the end of the file must exist.


With respect to the OP, the last solution would appear:

sed -Ez ':a;s/^([^\o000]*)(<TARGET>.*)[\o000]?/x\1\o000\2/
         tb;:b;s/^x{<NTH MATCH>}//;Ta
         s/\o000(<TARGET>)/<REPLACEMENT>/' 

Basically you will need to reverse parameter using tac and rev. See the explanation in the code...

# SET PARAMETERS

## The "BEGIN"/"END" are didactic strategies to avoid the loss of whitespace at the end and beginning of the string
read -r -d '' SOURCE << 'HEREDOC'
BEGIN
askdjisoadjsadmsadjnasjndnsakjn

jkjkljkljklkljlkjkljl
jkljkljkljkklj
hjkhjkhkjh

jkjkljkljklkljlkjkljl
jkljkljkljkklj
hjkhjkhkjh

jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
jdsjhfbjdhs
END
HEREDOC
SOURCE="${SOURCE:6:-3}"
TARGET="jdsjhfbjdhs"
REPLACE="IReplacedTheLast4th"
NTH_MATCH=4

# REVERSE PARAMETER CONTENT

TARGET=$(echo "x${TARGET}x" | tac | rev)

## Strategy to avoid losing whitespace at the end and beginning of the string
TARGET=${TARGET%?}
TARGET=${TARGET#?}

REPLACE=$(echo "x${REPLACE}x" | tac | rev)
REPLACE=${REPLACE%?}
REPLACE=${REPLACE#?}
SOURCE=$(echo "x${SOURCE}x" | tac | rev)
SOURCE=${SOURCE%?}
SOURCE=${SOURCE#?}

# USING STRING AS INPUT

echo "${SOURCE}" | sed -z 's/$TARGET/$REPLACE/$NTH_MATCH'
SED_RESULT=$(echo "x${SOURCE}x" | eval "sed -z 's/$TARGET/$REPLACE/$NTH_MATCH'")

## Undo reverse

SED_RESULT=$(echo "x${SED_RESULT}x" | tac | rev)
SED_RESULT=${SED_RESULT%?}
SED_RESULT=${SED_RESULT#?}

# USING FILE AS INPUT

echo "${SOURCE}" > ./my_file.txt

## Reverse file content input and undo on output to the same file
tac "./my_file.txt" | rev | eval "sed -z 's/$TARGET/$REPLACE/$NTH_MATCH'" | tac | rev > "./my_file.txt"

HOT: In this answer there is this implementation in a function with several interesting facilities for using with sed.

Thanks!

Related