Comment all lines from last commented line to line with 'foo'

Consider a text file users.txt:

#alice
#bob
charlie
dotan
eric

I need to comment everything from (exclusive) the last commented line until (inclusive) dotan. This is the result:

#alice
#bob
#charlie
#dotan
eric

Is there a nice sed oneliner to do this? I’ll be happy with any tool, not just sed, really.

Currently I am getting the line number of the last commented line as so:

$ cat -n users.txt | grep '#' | tail -n1
  2 #bob

I then add one and comment with sed:

$ sed -i'' '3,/dotan/ s/^/#/' users.txt

I know that I could be clever and put this all together with some bc into an ugly one-liner. Surely there must be a cleaner way?

Asked By: dotancohen

||

How about

perl -pe '$n=1 if s/^dotan/#$&/; s/^[^#]/#$&/ unless $n==1;' file

or, the same idea in awk:

awk '(/^dotan/){a=1; sub(/^/,"#",$1)} (a!=1 && $1!~/^#/){sub(/^/,"#",$1);}1; ' file
Answered By: terdon

If the existing commented lines form a single contiguous block, then you could match from the first commented line instead, commenting-out only those lines up to and including your end pattern that are not already commented

sed '/^#/,/dotan/ s/^[^#]/#&/' file

If the existing comments are not contiguous, then due to the greedy nature of the sed range match I think you would need to do something like

tac file | sed '/dotan/,/^#/ s/^[^#]/#&/' | tac

i.e. match upwards from the end pattern to the ‘first’ comment – obviously that’s not so convenient if you want an in-place solution though.

Answered By: steeldriver

You can handle both cases (commented lines in a single contiguous block or interspersed among uncommented lines) with a single sed invocation:

sed '1,/PATTERN/{/^#/{x;1d;b};//!{H;/PATTERN/!{1h;d};//{x;s/n/&#/g}}}' infile

This processes only the lines in the 1,/PATTERN/ range. It exchanges hold space w. pattern space every time a line is commented (so there is never more than one commented line in the hold buffer) and appends every line that is not commented to the Hold space (when on 1st line, 1d and respectively 1h are also needed to remove the initial empty line in the hold buffer).
When it reaches the line matching PATTERN, it also appends it to the Hold buffer, exchanges the buffers and then replaces every newline character in the pattern space with a newline and a # (that is, all lines in pattern space will now begin with #, including the first line as the first line in the hold space is always a commented line).
With a sample infile:

alice
#bob
bill
#charlie
ding
dong
dotan
jimmy
#garry

running:

sed '1,/dotan/{                   # if line is in this range    -start c1
/^#/{                             # if line is commented        -start c2
x                                 # exchage hold space w. pattern space
1d                                # if 1st line, delete pattern space
b                                 # branch to end of script
}                                 #                             -end c2
//!{                              # if line is not commented    -start c3
H                                 # append to hold space
/dotan/!{                         # if line doesn't match dotan -start c4
1h                                # if 1st line, overwrite hold space
d                                 # delete pattern space
}                                 #                             -end c4
//{                               # if line matches dotan       -start c5
x                                 # exchage hold space w. pattern space
s/n/&#/g                         # add # after each newline character
}                                 #                             -end c5
}                                 #                             -end c3
}' infile                         #                             -end c1

outputs:

alice
#bob
bill
#charlie
#ding
#dong
#dotan
jimmy
#garry

so it’s commenting only lines from (and excluding) #charlie up to (and including) dotan and leaving the other lines untouched.
Sure, this assumes there’s always at least one commented line before the line matching PATTERN. If that’s not the case you could add an additional check before the replacement: /^#/{s/n/&#/g}

Answered By: don_crissti

Here’s another sed:

sed  -e:n -e'/n#.*ndotan/!{$!{N;/^#/bn'      
-eb  -e} -e'/^#/s/(n)(dotan.*)*/1#2/g' 
-et  -e} -eP;D <in >out

That does as you ask. It just works on a stack – building it when necessary and for as long as necessary between occurences of commented lines, and dumping the old buffer in favor of the new commented line further on in input when it finds one. Picture…

enter image description here

Sorry, I don’t know why I did that. But it came to mind.

Anyway, sed spreads its buffers between each last commented line in any series, never retaining a single more in its buffer than is necessary to accurately track the last commented occurrence, and if at any time it encounters the last line while doing so it will attempt the final global execution statement and branch test the whole buffer out to be printed, else it will Print all of those lines it releases from its buffer as soon as it does.

I guess this is what brought the accordions to mind…

printf %s\n   #alice #bob charlie dotan eric 
               #alice #bob charlie dotan eric 
               #alice #bob charlie dotan eric |
sed  -e:n -e'l;/n#.*ndotan/!{$!{N;/^#/bn'     
-eb  -e} -e'/^#/s/(n)(dotan.*)*/1#2/g'  
-et  -e} -eP;D

#alice
#alicen#bob$
#alicen#bobncharlie$
#alicen#bobncharliendotan$
#alice
#bobncharliendotan$
#bobncharliendotanneric$
#bobncharliendotannericn#alice$
#bobncharliendotannericn#alicen#bob$
#bobncharliendotannericn#alicen#bobncharlie$
#bobncharliendotannericn#alicen#bobncharliendotan$
#bob
charliendotannericn#alicen#bobncharliendotan$
charlie
dotannericn#alicen#bobncharliendotan$
dotan
ericn#alicen#bobncharliendotan$
eric
#alicen#bobncharliendotan$
#alice
#bobncharliendotan$
#bobncharliendotanneric$
#bobncharliendotannericn#alice$
#bobncharliendotannericn#alicen#bob$
#bobncharliendotannericn#alicen#bobncharlie$
#bobncharliendotannericn#alicen#bobncharliendotan$
#bob
charliendotannericn#alicen#bobncharliendotan$
charlie
dotannericn#alicen#bobncharliendotan$
dotan
ericn#alicen#bobncharliendotan$
eric
#alicen#bobncharliendotan$
#alice
#bobncharliendotan$
#bobncharliendotanneric$
#bob
#charlie
#dotan
eric

There’s only one difference between this command and the one above and that is the look command at the top. When we look at sed‘s pattern space as it works we can get a better idea of what goes on behind the scenes and a better understanding of how to direct its efforts.

In this case we can watch sed stack input until has found a second occurrence of n#.*ndotan in input, and that when it starts printing the previous out a line at a time. It’s kinda cool. I learned a lot working on this.

Answered By: mikeserv
Categories: Answers Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.