4

Currently, I have files which contain sections like this:

code statement1
code statement2
# BEGIN SOMENAME
some code
some other code
# END SOMENAME
code statement n +1
code statement n +1

What I want to do is to comment out what is between

# BEGIN SOMENAME

and

# END

so that in the end, it looks like this:

code statement1
code statement2
# BEGIN SOMENAME
# some code
# some other code
# END SOMENAME
code statement n +1
code statement n +1

Can I achieve this with awk or sed? And can I reverse it easily with an operation that "comments in" again?

What I want to avoid is making mistakes, so if the lines are already commented out they should be left alone. Also, in "comment in", it should not try to do something if the lines between the end and begin do not start with a #.


Found a possible solution:

awk '
    BEGIN { 
        i=0; 
        line_with_no_comment_found=0 
    }
    
    /^# END/ { 
        m=0;
        if ( line_with_no_comment_found == 1 ) { 
            for (var in a) print "# "a[var] 
        } else { 
            for (var in a) print a[var] 
        }
        delete a;
        i=0;
        line_with_no_comment_found=0;
    }
    
    /^# / { 
        if (m==0) { 
            print 
        } else { 
            a[i++]=$0; 
        }
    }
    
    !/^# / { 
        if (m==0) { 
            print 
        } else { 
            a[i++]=$0; 
            line_with_no_comment_found=1
        }
    }
    
    /^# BEGIN ([a-zA-Z_])([1-9][0-9]*)*/ {
        m=1;
    }
    END { }
'<<EOF
2
  • maybe, because this old code has mistakes like that multiple begin lines (copy/paste bugs...?) after another, and you just need ignore the first one or sometime the begin starts and the file ends.... i'd rather not post this script, its competly broken now....
    – Mandragor
    Commented Feb 2, 2016 at 15:34
  • you are right. added this to challenges. Sorry got tired, and it seems always when i found a partial solution, all other things stop working
    – Mandragor
    Commented Feb 2, 2016 at 15:51

3 Answers 3

1

This script works for me. I tested it in GNU Awk 4.0.1, but should work in Nawk as well.

awk 'BEGIN {
    # action=0: uncomment
    # action=1: comment
    action=0
    in_optional_code_block=0
}
{
    if ($0 ~ /^# BEGIN/) {
        in_optional_code_block=1
    } else if ($0 ~ /^# END/) {
        in_optional_code_block=0
    } else if (in_optional_code_block) {
        if (action) {
            if ($0 !~ /^#/) {
                $0 = "# " $0
            }
        } else {
            if ($0 ~ /^#/) {
                sub(/^# ?/, "")
            }
        }
    }
}
1'

I also wrote a small accompanying shell script:

#!/usr/bin/env sh

syntax_error() {
    echo "Usage: `basename \"$0\"` [comment|uncomment] file" >&2
    exit 1
}

case "$1" in
    0|uncomment) action=0; ;;
    1|comment) action=1; ;;
    *) syntax_error; ;;
esac
shift
if [ -z "$@" ]; then syntax_error; fi

awk 'BEGIN {
    action='$action'
    in_optional_code_block=0
}
{
    if ($0 ~ /^# BEGIN/) {
        in_optional_code_block=1
    } else if ($0 ~ /^# END/) {
        in_optional_code_block=0
    } else if (in_optional_code_block) {
        if (action) {
            if ($0 !~ /^#/) {
                $0 = "# " $0
            }
        } else {
            if ($0 ~ /^#/) {
                sub(/^# ?/, "")
            }
        }
    }
}
1' "$@" > "[email protected]"
if [ $? -eq 0 ]; then mv "[email protected]" "$@"; fi

(If you have GNU Awk 4.1.0 or later, you can use the -i flag instead of the move construction at the end.)

1
  • @Mandragor If SOMENAME in the opening and closing comments have to match, give me a shout.
    – Midgard
    Commented Feb 5, 2016 at 21:54
0

Addressing the "possible solution" in the question, there are a few issues in your suggested code:

  • The loop for (var in a) is not guaranteed to iterate over the indexes of a in order, which will have the consequence that the lines that you comment might be outputted in a random order.
  • Your patterns for detecting the BEGIN and END lines do not attempt to pair up the label used on either line, so that # BEGIN FOO may be ended by # END BAR.
  • There are unnecessary duplication of code between the /^# / block and the !/^# / block.
  • An empty END block is needlessly present.
  • The code does not un-comment.

To easier be able to un-comment the commented lines in the sections that are affected by the commenting, we also add extra # characters to the start and end marker lines:

sed '/^# BEGIN SOMENAME$/,/^# END SOMENAME$/ s/^/#/' file

This inserts a single # character at the start of each line between the # BEGIN SOMENAME and # END SOMENAME lines, including on those marker lines.

This would, for the given text, result in

code statement1
code statement2
## BEGIN SOMENAME
#some code
#some other code
## END SOMENAME
code statement n +1
code statement n +1

If # were only inserted on lines where that character was not already the first character, it would be unreasonably difficult to reverse the operation as there is no log of what lines were originally commended out between the marker lines.

To un-comment this:

sed '/^## BEGIN SOMENAME$/,/^## END SOMENAME$/ s/#//' file

This affects only the lines commended using the first sed command, returning them to their original state by removing the inserted # character at the start of each line.

Would you want to comment out the lines using # followed by a space character, then use

sed '/^# BEGIN SOMENAME$/,/^# END SOMENAME$/ s/^/# /' file

for commenting, and

sed '/^# # BEGIN SOMENAME$/,/^# # END SOMENAME$/ s/# //' file

for un-commenting.


The equivalent operations using awk instead of sed:

For commenting:

awk '$0 == "# BEGIN SOMENAME", $0 == "# END SOMENAME" { $0 = "#" $0 }; 1' file

For un-commenting:

awk '$0 == "## BEGIN SOMENAME", $0 == "## END SOMENAME" { $0 = substr($0, 2) }; 1' file

In both these commands, I use string comparisons instead of regular expressions since these are generally faster, and there's no need to involve any pattern matching. The substr() call in the second command (for un-commenting) would return the input string minus the first character. The lone trailing 1 causes the current line to be output.

0

This popped up today. The solutions proposed seem overly complicated:

/# BEGIN SOMENAME/ { prefix="# "; }
/# END SOMENAME/ { prefix=""; }
{ print prefix $0; }

(although the existing comments will be double prefixed).

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .