I am trying to match lines that do not contain a sequence of characters using sed. According to the sed documentation (https://www.gnu.org/software/sed/manual/sed.html), you use ()
in sed when using the ERE (-E option) to create groups and then {}
to capture repetition of the preceding token. However, I want to match lines that do not contain this group (which also includes other things that I want to match).
Here is the contents of test_file:
Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin
#Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults secure_path = /sbin:/bin:/home/tlytle/bin:/usr/sbin:/usr/bin
These are just variations of the same line to test my sed command on. In essence, I want to modify lines that do not contain the path /home/tlytle/bin
by appending :/home/tlytle/bin
.
Here is my thought process as I am building up my ERE for sed:
First, I want to capture all lines that contain /home/tlytle/bin.
$ sed -E '\|/home/tlytle/bin| s|$|:/home/tlytle/bin|' test_file
Output:
Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin
#Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults secure_path = /sbin:/bin:/home/tlytle/bin:/usr/sbin:/usr/bin:/home/tlytle/bin
So far so good. sed appended :/home/tlytle/bin
to the end of the only line that already contained :/home/tlytle/bin
. Now, I want to create a group for that. So, I do this:
$ sed -E '\|(/home/tlytle/bin)| s|$|:/home/tlytle/bin|' test_file
Output:
Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin
#Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults secure_path = /sbin:/bin:/home/tlytle/bin:/usr/sbin:/usr/bin:/home/tlytle/bin
Still looks good. Now, just as a test, I want to match it 1 or more times. I could use a '+' here, but I want to test the repetition construct just ensure I am not losing my mind:
$ sed -E '\|(/home/tlytle/bin){1,}| s|$|:/home/tlytle/bin|' test_file
Output:
Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin
#Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults secure_path = /sbin:/bin:/home/tlytle/bin:/usr/sbin:/usr/bin:/home/tlytle/bin
Looks like it is functioning good to me. Now, I want this to match exactly zero of these. And this is where it breaks down:
$ sed -E '\|(/home/tlytle/bin){0}| s|$|:/home/tlytle/bin|' test_file
Output:
Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/home/tlytle/bin
#Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/home/tlytle/bin
Defaults secure_path = /sbin:/bin:/home/tlytle/bin:/usr/sbin:/usr/bin:/home/tlytle/bin
It looks like the ERE matched every line. I would have thought it would have only matched lines that did not contain /home/tlytle/bin
.
Can someone explain to me why this does not do what I think it should do? Is there some other ERE construct I should be using?
As others have said, just use a bang to fix your sed:
sed '\|/home/tlytle/bin|!s|$|:/home/tlytle/bin|' test_file
And if you have the concerns about special characters needing to be escaped, use perl and its ability to escape with \Q..\E
:
perl -pe '$p="/home/tlytle/bin"; s/$/:$p/ unless /\Q$p\E/' test_file