Search code examples
regexbashsed

How to match a group exactly zero times


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?


Solution

  • 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