Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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?

share|improve this question

4 Answers 4

up vote 5 down vote accepted

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
share|improve this answer
    
Perfect, thank you! –  dotancohen Jul 21 '14 at 13:36

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.

share|improve this answer
    
Thank you, I've learned from this. –  dotancohen Jul 21 '14 at 20:51

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}

share|improve this answer
    
Thank you, I will have quite a bit to learn from this answer! –  dotancohen Jul 13 at 12:02
    
Hang on, I must have screwed up. This isn't about the last series of commented lines? No, I get it, it is. The last series + dotan. Pretty damn clever. –  mikeserv Jul 13 at 16:03
1  
You always find the best questions. Damned dotan had me thrown for awhile - maybe still does, i havent tested it yet. thanks, don. –  mikeserv Jul 13 at 16:48

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
#alice\n#bob$
#alice\n#bob\ncharlie$
#alice\n#bob\ncharlie\ndotan$
#alice
#bob\ncharlie\ndotan$
#bob\ncharlie\ndotan\neric$
#bob\ncharlie\ndotan\neric\n#alice$
#bob\ncharlie\ndotan\neric\n#alice\n#bob$
#bob\ncharlie\ndotan\neric\n#alice\n#bob\ncharlie$
#bob\ncharlie\ndotan\neric\n#alice\n#bob\ncharlie\ndotan$
#bob
charlie\ndotan\neric\n#alice\n#bob\ncharlie\ndotan$
charlie
dotan\neric\n#alice\n#bob\ncharlie\ndotan$
dotan
eric\n#alice\n#bob\ncharlie\ndotan$
eric
#alice\n#bob\ncharlie\ndotan$
#alice
#bob\ncharlie\ndotan$
#bob\ncharlie\ndotan\neric$
#bob\ncharlie\ndotan\neric\n#alice$
#bob\ncharlie\ndotan\neric\n#alice\n#bob$
#bob\ncharlie\ndotan\neric\n#alice\n#bob\ncharlie$
#bob\ncharlie\ndotan\neric\n#alice\n#bob\ncharlie\ndotan$
#bob
charlie\ndotan\neric\n#alice\n#bob\ncharlie\ndotan$
charlie
dotan\neric\n#alice\n#bob\ncharlie\ndotan$
dotan
eric\n#alice\n#bob\ncharlie\ndotan$
eric
#alice\n#bob\ncharlie\ndotan$
#alice
#bob\ncharlie\ndotan$
#bob\ncharlie\ndotan\neric$
#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.

share|improve this answer
    
Very nice, thank you! The final paragraph with explanations is terrific, I will spend quite some time learning from this post as well. Nice stack! –  dotancohen Jul 13 at 16:16
1  
@dotancohen - this was a really good question. Have a look at the edit to see the stack. –  mikeserv Jul 13 at 18:33
2  
I notice in the edit history the entry Handle many dotans. I'm sure that this is my wife's worst nightmare. –  dotancohen Jul 13 at 19:08
1  
@dotancohen - yeah, this was a tough one. Stuff like #\ndotan\ndotan is hard for these things. I mean it when I say this a good question. I think I got it just about perfect, but one problem you could run into is if your comment blocks are separated by a 1000 lines - that will slow it down. You might stick something like s/\n/&/150;t in before the first /\n# thing to break out the buffer if it spans 150 lines, for example. And anyway, maybe it's just what she's been waiting for all along! –  mikeserv Jul 13 at 19:13

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.