0

There are several questions about matching between only the first occurrence of two patterns (e.g., this) — but they all seem to rely on exit, which is no good, as I want to keep processing the file.

What I want to do is modify the first occurrence of a range between patterns (but not other occurrences) and do other awk foo. Specifically, I'm trying to comment in/out some sections of a dovecot conf file. In a truly ideal world, I'd want a single line* that makes sure the userdb { driver = prefetch } block is un-commented, and all other userdb { } blocks are commented. But since the file is at least a little predictable, I was aiming for uncommenting the first commented block, and then commenting all others.

I have

... some stuff that should be echoed as is ...
#userdb {
#  driver = prefetch
#}
... more stuff
userdb {
  driver = sql
  args = /etc/dovecot/dovecot-sql.conf.ext
}
... still more stuff
#userdb {
  #driver = static
  #args = uid=vmail gid=vmail home=/var/vmail/%u
#}

And I want

... original stuff ...
userdb {
  driver = prefetch
}
... more stuff
#userdb {
#  driver = sql
#  args = /etc/dovecot/dovecot-sql.conf.ext
#}
... still more stuff
#userdb {
  #driver = static
  #args = uid=vmail gid=vmail home=/var/vmail/%u
#}

I thought I was going to get somewhere with setting a flag in awk, thusly:

awk '/^#userdb/,/^#}/ && !ididit {
        print substr($0,2);
        next;
        ididit=1;
    }
    /^userdb/,/}/ {
        print "#", $0;
        next
    }
    {print}' auth-sql.conf.ext

Which was oh-so-close, but setting my ididit flag (I added a bunch of prints, just to make sure it was actually getting set) doesn't cut it:

... original stuff ...
userdb {
  driver = prefetch
}
... more stuff
# userdb {
#   driver = sql
#   args = /etc/dovecot/dovecot-sql.conf.ext
# }
... more stuff ...
userdb {
 #driver = static
 #args = uid=vmail gid=vmail home=/var/vmail/%u
}

Note the last userdb {} block managed to get itself uncommented. This was quite vexing, until I found deep in the bowels of GNU awk docs this little gem:

echo Yes | awk '/1/,/2/ || /Yes/'

The author of this program intended it to mean (/1/,/2/) || /Yes/. However, awk interprets this as /1/, (/2/ || /Yes/). This cannot be changed or worked around; range patterns do not combine with other patterns.

So, my little && !ididit is a non-starter.

Any suggestions on how I can make this (or better, my "In a truly ideal world", from above) happen on a single line*? perl is not an option, but sed or even bash could be.

* "a single line" can be a fairly complex line, though I'd still like it to be understandable, and throw a reasonable exit code — I'm trying to put this into an AWS CloudFormation template.

FWIW

$awk -V
GNU Awk 4.0.2

Thanks!

  • Can you please (1) write a clearer, more concise and precise explanation of what you want to do, (2) put it before the examples, (3) make the examples shorter and clearer, and (4) break your awk command into shorter lines, so it doesn’t require horizontal scrolling? – G-Man Says 'Reinstate Monica' Apr 16 '19 at 15:16
  • Done. Hopefully that's better/clearer. Let me know if you want further edits. – philolegein Apr 16 '19 at 15:29

1 Answers1

0

There seems to be a philosophy that you should do as much testing as possible in the “patterns” in awk scripts (i.e., the things before the {).  I was programming in C and other languages long before I encountered awk, so I’m not reluctant to use an if statement (or three) when it seems appropriate.

So here’s what I came up with:

awk '/userdb/,/}/ {
        if (!comment) {                         # Uncomment the first one
                sub("#", "")
                print
                if ($0 ~ /}/) comment=1         # All others get commented
        } else {
                if ($0 ~ /^[[:blank:]]*#/) {    # Already commented out?
                        print
                } else {
                        print "#" $0
                }
        }
        next
      }
      { print }
    '

It’s clearly based on your code.  comment is a variable analogous to your ididit; it is initially 0 (false), and gets set to 1 (true) after the first block has been processed.

Within a /userdb/ - /}/ block,

  • If we’re uncommenting,
    • use sub to remove a #, if any.  This is safer than unconditionally ripping off the first character — what if the code is already uncommented?
    • When we see the } (marking the end of the block), set the comment flag, so all future matches will be commented out.
  • If we’re commenting,
    • if the line is already commented, just print it as is,
    • otherwise, put a # in front.

Note that the uncommenting code assumes that the first block does not contain any comments.  For example,

userdb {
  driver = prefetch     # We want to keep this uncommented.
}

would be changed to

userdb {
  driver = prefetch      We want to keep this uncommented.
}

which looks like it would be an error.