How to let sed un-comment one of multiple paragraphs separated by newlines?

Clarification: Using GNU sed.

Status quo – ~/.screenrc, last lines:

# name synthpop
# screen -t script emacs -nw /home/$USER/bin/synthpop

# name thrashmetal
# screen -t script emacs -nw /home/$USER/bin/thrashmetal
# number 1
# split -v
# focus
# chdir "/home/$USER/thrashmetal/src"
# screen -t scr watch nl asdkjlek.html
# number 2
# focus
# split
# focus
# screen -t output watch thrashmetal asdkjlek.html
# number 3
# focus
# split
# focus
# chdir "/home/$USER/thrashmetal/plan"
# screen -t soll watch less soll
# number 4
# focus

# name darkwave
# screen -t script emacs -nw /home/$USER/bin/darkwave

Aim:

# name synthpop
# screen -t script emacs -nw /home/$USER/bin/synthpop

name thrashmetal
screen -t script emacs -nw /home/$USER/bin/thrashmetal
number 1
split -v
focus
chdir "/home/$USER/thrashmetal/src"
screen -t scr watch nl asdkjlek.html
number 2
focus
split
focus
screen -t output watch thrashmetal asdkjlek.html
number 3
focus
split
focus
chdir "/home/$USER/thrashmetal/plan"
screen -t soll watch less soll
number 4
focus

# name darkwave
# screen -t script emacs -nw /home/$USER/bin/darkwave

My current attempt, which almost does the job but fails stopping as soon as the line comes, which is just a new line without any comment sign or alphanumerical characters:

sed 's+^# (name thrashmetal)+1+;:a;s+# ++;ta' .screenrc

Explanation, intention:

  • ' start of a sequence of things to do

  • s replace

  • + use "+" as delimiter

  • # … go through line by line until you find a line starting with "# "…

  • ( start group for later back-reference

  • name thrashmetal and continuing with "name thrashmetal"

  • ) end of group

  • +1+ replace first match within that line with just the group part, without the "# "

  • ; then, from that point on, do another thing…

  • :a do a loop called "a"

  • ;s where for every following line, line by line, you replace…

  • + set "+" as delimiter

  • # the first match of "# " within this line, irrespective of what follows it within this line…

  • ++ with nothing, effectively deleting it

  • ;ta do that as long as you fail doing it, which should be as soon as you have reached the end of the paragraph and then the 2 newlines separating the thrashmetal-block from the darkwave-block, and then go back to where "a" was

  • ' end of a sequence of things to do

  • .screenrc do said sequence of things regarding the file ".screenrc" in my current directory – my home directory

Standard output:

# name synthpop
# screen -t script emacs -nw /home/$USER/bin/synthpop

name thrashmetal
screen -t script emacs -nw /home/$USER/bin/thrashmetal
number 1
split -v
focus
chdir "/home/$USER/thrashmetal/src"
screen -t scr watch nl asdkjlek.html
number 2
focus
split
focus
screen -t output watch thrashmetal asdkjlek.html
number 3
focus
split
focus
chdir "/home/$USER/thrashmetal/plan"
screen -t soll watch less soll
number 4
focus

name darkwave
screen -t script emacs -nw /home/$USER/bin/darkwave

How to fix this? What am I doing wrong that the loop doesn’t come to a halt as soon as it reaches the end of the thrashmetal-paragraph, but simply hops over to the next paragraph – the darkwave-paragraph – and continues?

Anything in that loop must be wrong, I guess…
Maybe the "t", but "T" or "b" don’t cut it, either…

Asked By: futurewave

||

This script seems to do what you want:

H
/^$/ T show
$ T show
d

:show
x
s/n//
/^# name thrashmetal/ {
  s/# //
  s/n# /n/g
}

We are effectively splitting things up into blank-line delimited chunks. When we find a chunk that begins with # name thrashmetal, we remove all the # strings at the the beginning of lines.

Given your sample input, this produces as output:

$ sed -f filter.sed < example.conf
# name synthpop
# screen -t script emacs -nw /home/$USER/bin/synthpop

name thrashmetal
screen -t script emacs -nw /home/$USER/bin/thrashmetal
number 1
split -v
focus
chdir "/home/$USER/thrashmetal/src"
screen -t scr watch nl asdkjlek.html
number 2
focus
split
focus
screen -t output watch thrashmetal asdkjlek.html
number 3
focus
split
focus
chdir "/home/$USER/thrashmetal/plan"
screen -t soll watch less soll
number 4
focus

# name darkwave
# screen -t script emacs -nw /home/$USER/bin/darkwave

The sed script with comments:

# Append current line to the hold space
H

# If we find a blank line or reach EOF, print out the
# current chunk
/^$/ T show
$ T show

# Delete the current line (we'll print it later in the
# show routine)
d

:show
# Swap the pattern space and the hold space
x

# Remove the initial "n" that was added when we appended
# the first line with the H command
s/n//

# If this is our target chunk, remove the comments
/^# name thrashmetal/ {
  s/# //
  s/n# /n/g
}
Answered By: larsks
sed '/^# name thrashmetal$/,/^$/ s/^# //' file

This applies a substitution that deletes an initial # and a subsequent space character from each line in a range.

The range starts with the line matching ^# name thrashmetal$ and ends with the first subsequent line matching ^$ (empty line).

Or, directly translated into awk:

awk '/^# name thrashmetal$/,/^$/ { sub(/^# /, "") }; 1' file

Here, the trailing 1 is short for { print $0 } or { print } and causes the current (possibly modified) line to be outputted (this printing of the text is implicit in the sed script).


For anyone interested in the ed editor, you would solve this with the following editing command:

/^# name thrashmetal$/;.,/^$/ s/^# //

This moves the cursor to the first line of the desired paragraph and applies the substitution to that line and all later lines until we reach an empty line.

We can’t use

/^# name thrashmetal$/,/^$/ s/^# //

… as the addresses are computed relative to the current line, and the current line is at the end of the file when we start the editor. Since we’re at the end of the file, the range in the command would be invalid as the /^$/ address would be an earlier line than the start address (relative to the current line, the first empty line comes before the start of our paragraph).


Your own approach, corrected (with the delimiters changed to the default from pluses and splitting up into multiple expressions, which is more portable):

sed -e '/^# (name thrashmetal)/!b' -e 's//1/' -e :a -e n -e 's/^# //' -e ta file

Instead of an initial substitution, I let the matching of the paragraph’s initial line determine whether the rest of the editing script is skipped or not. It is skipped with b if the line is not encountered.

If the line is found, we modify it and create our label a. We then print the current buffer and replace its contents with the next line using n. The loop continues as before, removing the # substring and looping back to the label if the buffer’s contents change.

The main issue with your original attempt,

sed 's+^# (name thrashmetal)+1+;:a;s+# ++;ta' .screenrc

… is that the "a-loop" is unconditional and, therefore, applies to every single line of the input, regardless of whether the initial substitution succeeded or failed (you show the first two lines as being unaffected by your command, but I think this is a copy-paste error). The loop will also not read the next line in, so it will only ever run once on each line (unless multiple # substrings are found on a line).

My fix involves splitting up your initial substitution into a test and a substitution, and then ensuring that the "a-loop" reads the next line of input until the substitution fails. I also will not replace multiple or embedded (internal) # strings on any one line, only the first at the start of the line.

Answered By: Kusalananda

You could also consider using awk:

$ awk -v RS= -v ORS='nn' '/thrashmetal/{sub(/^# /,"");gsub(/n# /,"n",$0)}1'  .screenrc
# name synthpop
# screen -t script emacs -nw /home/$USER/bin/synthpop 

name thrashmetal
screen -t script emacs -nw /home/$USER/bin/thrashmetal
number 1
split -v
focus
chdir "/home/$USER/thrashmetal/src"
screen -t scr watch nl asdkjlek.html
number 2
focus
split
focus
screen -t output watch thrashmetal asdkjlek.html
number 3
focus
split
focus
chdir "/home/$USER/thrashmetal/plan"
screen -t soll watch less soll
number 4
focus 

# name darkwave
# screen -t script emacs -nw /home/$USER/bin/darkwave 

Here by setting RS (input record separator) to blank you deal with paragraphs as if they were single lines.

Answered By: user9101329

Using any awk:

$ awk '/^# name /{f=($3 == "thrashmetal")} f{sub(/^# /,"")} 1' file
# name synthpop
# screen -t script emacs -nw /home/$USER/bin/synthpop

name thrashmetal
screen -t script emacs -nw /home/$USER/bin/thrashmetal
number 1
split -v
focus
chdir "/home/$USER/thrashmetal/src"
screen -t scr watch nl asdkjlek.html
number 2
focus
split
focus
screen -t output watch thrashmetal asdkjlek.html
number 3
focus
split
focus
chdir "/home/$USER/thrashmetal/plan"
screen -t soll watch less soll
number 4
focus

# name darkwave
# screen -t script emacs -nw /home/$USER/bin/darkwave
Answered By: Ed Morton

Using Raku (formerly known as Perl_6)

~$ raku -ne 'if / thrashmetal /fff^/ # s name / { .subst(/^ "# " /).put } else { $_.put };'  file

OR:

~$ raku -ne 'put (/ thrashmetal /fff^/ # s name /) ?? $_.subst(/^ "# " /)  !! $_ ;'  file

Raku is a programming language in the Perl-family. You can use Raku’s -ne awk-like non-autoprinting command line flags and an if/else statement to alter the chosen lines.

The key here is using Raku’s /…ON…/fffˆ/…OFF…/ "flip-flop" operator, which flips ON when the first recognition domain is matched, then flips OFF when the second recognition domain is matched. Based on fff, here we add a trailing ^ caret which instructs Raku to drop the second matched line.

So you can match the desired commented-out record/paragraph in one go, using thrashmetal for the first match and # s name for the second match. Uncomment the identified record with .subst(/^ "# " /).

Sample Input:

# name synthpop
# screen -t script emacs -nw /home/$USER/bin/synthpop

# name thrashmetal
# screen -t script emacs -nw /home/$USER/bin/thrashmetal
# number 1
# split -v
# focus
# chdir "/home/$USER/thrashmetal/src"
# screen -t scr watch nl asdkjlek.html
# number 2
# focus
# split
# focus
# screen -t output watch thrashmetal asdkjlek.html
# number 3
# focus
# split
# focus
# chdir "/home/$USER/thrashmetal/plan"
# screen -t soll watch less soll
# number 4
# focus

# name darkwave
# screen -t script emacs -nw /home/$USER/bin/darkwave

Sample Output:

# name synthpop
# screen -t script emacs -nw /home/$USER/bin/synthpop

name thrashmetal
screen -t script emacs -nw /home/$USER/bin/thrashmetal
number 1
split -v
focus
chdir "/home/$USER/thrashmetal/src"
screen -t scr watch nl asdkjlek.html
number 2
focus
split
focus
screen -t output watch thrashmetal asdkjlek.html
number 3
focus
split
focus
chdir "/home/$USER/thrashmetal/plan"
screen -t soll watch less soll
number 4
focus

# name darkwave
# screen -t script emacs -nw /home/$USER/bin/darkwave

https://docs.raku.org/routine/fff%5E
https://raku.org

Answered By: jubilatious1
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.