1

I'm editing my .uncrustify.cfg file to make it more readable. I just want to reformat like this:

Two lines in:

# Add or remove between the parens in the function type: 'void (*x)(...)'
sp_after_tparen_close      = ignore   # ignore/add/remove/force

One line out:

sp_after_tparen_close      = ignore   # ignore/add/remove/force#   Add or remove between the parens in the function type: 'void (*x)(...)'

Perl seems to be the way to go, but the syntax overwhelms me. When I have a spare decade, I'll learn it ;-)

To make it a bit more generic:

Two lines in:

#a comment line
some code

One line out:

some code # a comment line

==========================

John: Two lines done by hand:

nl_while_brace             = ignore   # I,A,R,F     # Add or remove newline between 'while' and '{'
nl_scope_brace             = ignore   # I,A,R,F     # Add or remove newline between 'scope (x)' and '{' (D)

... two pairs that didn't combine using your awk:

# Add or remove newline between 'unittest' and '{' (D)
nl_unittest_brace          = ignore   # I,A,R,F

# Add or remove newline between 'version (x)' and '{' (D)
nl_version_brace           = ignore   # I,A,R,F
8
  • It's probably much simpler if you just do this with a script (reading from stdin, writing to stdout) than try and cobble something with perl -e ... (which does not save any time unless you are sure about what you are doing). You should also explain the nature of the change more generically, if appropriate, since changing exactly those lines is different than changing lines which meet a certain criteria.
    – goldilocks
    Oct 20, 2014 at 18:32
  • How big is the file? [Is it smaller than 100KB?]
    – AnFi
    Oct 20, 2014 at 18:56
  • Use the {} code tool in the SO editor to mark your literal code blocks.
    – Barmar
    Oct 20, 2014 at 19:16
  • @goldilocks: If I see a solution to the above, it will give me all I need to proceed, the 'generics' I'm sure I can handle, the line pairs in that file are all very similar. Oct 20, 2014 at 20:21
  • The file is smaller than 100KB. Oct 20, 2014 at 20:21

3 Answers 3

2
sed '/^#/N;s/\(.*\)\n\([^#].*\)/\2 \1/;P;D'

That will handle your simple example in the question - any commented line that is followed by a line that is not a comment and consists of at least one character will be appended to the line that follows it.

So, running your example through it and the output is:

sp_after_tparen_close      = ignore   # ignore/add/remove/force # Add or remove between the parens in the function type: 'void (*x)(...)'

Running @John1024's example through it and the output is:

#
# Some comments
#

sp_after_tparen_close      = ignore   # ignore/add/remove/force # Add or remove between the parens in the function type: 'void (*x)(...)'

some code #a comment line
more code

# comment one
# comment two
still more code # comment three

To handle conditions like these, sed needn't loop. In this case the only lines that can possibly contain a \newline character are those that begin with a #hash because those are the only lines to which sed will add one.

When sed encounters a line beginning with a #hash it pulls in the Next input line and appends it to pattern space. sed then attempts to s///ubstitute:

  • \(.*\) - as much as possible referenced as \1 immediately followed by...
  • \n - a newline character immediately followed by...
  • \([^#].*\) - at least one character that is not a #hash and everything remaining in pattern space...
  • with \2 \1.

sed then Prints pattern space up to the first occurring \newline character and Deletes same before starting over to try again with what remains (if any).

7
  • Works perfect :-) And, I'm trying to learn sed anyway. But we need to sort out why John's 'awk' didn't work sometimes, but was perfect other times. Oct 21, 2014 at 1:06
  • @rayandrews - I assure you, you will not regret learning sed fundamentals. Because sed is a more fundamental tool than either awk or perl anyway, grasping sed's line-cycle before tackling either of the other two is probably the best way to go. sed tends to simplify situations that the other two can complicate. Have a look here for an example.
    – mikeserv
    Oct 21, 2014 at 2:21
  • Yeah, I've been seding for a while, but this is my first attempt to handle more than one line. I'd rather improve my seding than start to learn awk or perl simply because I don't have the brain power for all three. Oct 21, 2014 at 2:42
  • @rayandrews - get cozy with N,P,D - they can make multiline sed easy. For example, it is no big deal to widen sed's pattern-space window on a file from 1 line to 3 lines: sed '1N;N;some rules;P;D. That drops one line every cycle and adds a new one every cycle, but each cycle addresses 3 lines at a time.
    – mikeserv
    Oct 21, 2014 at 3:03
  • 1
    Ha! I already have that doc bookmarked. Nothing beats learning by working on real problems, eh? Oct 21, 2014 at 21:56
1

I believe that this does what you want:

awk '/^[[:space:]]*[^#]/ && last ~ /^#/ {printf "%s %s",$0,last; last="";next} {print last; last=$0} END{print last}' sample.cfg

As an example, suppose that we have this input file:

#
# Some comments
#

# Add or remove between the parens in the function type: 'void (*x)(...)'
sp_after_tparen_close      = ignore   # ignore/add/remove/force

#a comment line
some code
more code

# comment one
# comment two
# comment three
still more code

The output is:

$ awk '/^[[:space:]]*[^#]/ && last ~ /^#/ {printf "%s %s",$0,last; last="";next} {print last; last=$0} END{print last}' uncrustify.cfg

#
# Some comments
#

sp_after_tparen_close      = ignore   # ignore/add/remove/force # Add or remove between the parens in the function type: 'void (*x)(...)'

some code #a comment line
more code

# comment one
# comment two
still more code # comment three

How it works

awk implicitly loops over every line in a file.

This script uses a single variable, last, which holds the previous line. In brief, as we go through each line, if the last line is a comment and the current line isn't, then we print out both lines together. Otherwise, we print out the last line.

  • /^[[:space:]]*[^#]/ && last ~ /^#/ {printf "%s %s",$0,last; last="";next}

    If (a) this line is not a comment, and (b) the previous (last) line is a comment, then combine the last and current line and print them out. After that, last to empty. Then, skip the rest of the commands and jump to start over on the next line.

  • {print last; last=$0}

    Otherwise, print the last line. Update last with the contents of the current line.

  • END{print last}

    After we are done looping through all the lines in the file, print the contents of the last line.

Another example

Consider this input:

# Add or remove newline between 'unittest' and '{' (D)
nl_unittest_brace          = ignore   # I,A,R,F

# Add or remove newline between 'version (x)' and '{' (D)
nl_version_brace           = ignore   # I,A,R,F

The output is:

$ awk '/^[[:space:]]*[^#]/ && last ~ /^#/ {printf "%s %s",$0,last; last="";next} {print last; last=$0} END{print last}' new

nl_unittest_brace          = ignore   # I,A,R,F # Add or remove newline between 'unittest' and '{' (D)

nl_version_brace           = ignore   # I,A,R,F # Add or remove newline between 'version (x)' and '{' (D)
6
  • +1 I know this is not in the spec, but curious as to what this will do with multi-line comments.
    – goldilocks
    Oct 20, 2014 at 20:55
  • @goldilocks Thanks. Only the last comment is appended to the code line. Otherwise, one could end up with some very very long lines.
    – John1024
    Oct 20, 2014 at 21:01
  • @John1024: Geez, it was working perfectly then ground to a halt hitting this: # Don't split one-line braced assignments - 'foo_t f = { 1, 2 };' nl_assign_leave_one_liners = false # false/true ...........Of course that doesn't format ... the command line starts with 'nl_assign ...' Oct 20, 2014 at 23:27
  • @rayandrews Hmmm. Would you put that input example, along with its desired output, in the question? Inside a comment, all the formatting is lost and I can't tell where the lines are supposed to break.
    – John1024
    Oct 21, 2014 at 0:40
  • @John1024, ok. I've tried trimming the file down to just the problem lines, but it still won't bite. Oct 21, 2014 at 0:59
1

As per my comment, it is easiest to develop something like this in a short script rather than fiddling with command-one-liners. Also, you can keep it.

#!/usr/bin/perl
use strict;
use warnings FATAL => qw(all);

my $buffer = "";
my $linesBuffered = 0;

while (<STDIN>) {
# Check if this line is a just a comment.
    if ($_ =~ /^\s*#/) {
    # Assume multi-line comments should not be appended.
        if ($buffer) { $buffer .= $_ }
        else { $buffer = $_ }
        $linesBuffered++;
    } else {
        if ($linesBuffered > 1) {
        # Print multi-line comment.
            print "$buffer$_";
        # Reset buffer.
            $buffer = "";
            $linesBuffered = 0;
        } elsif ($buffer) {
        # Print buffered line with comment trailing.
            chomp $_;
            print "$_ $buffer";
        # Reset buffer.
            $buffer = "";
            $linesBuffered = 0;
        } else { print $_ }
    }
}

print $buffer if ($buffer);  

This could be used, e.g., ./filter.pl < .uncrustify.cfg > .uncrustify.copy. Notice that is not done in place so you would have to cp .uncrustify.copy .uncrustify.cfg afterward if you are happy. Since it reads from standard input, you can test it:

> ./filter.pl
what  <- stdin
what  <- stdout

Here it spit out the line immediately since it is not a comment. I won't bother indicating stdin and stdout in the next examples.

#okay
then
then #okay

In this case it buffered the comment line and appended it to the next (non-comment) line.

#foo
#bar
#foo
#bar

In this case it spit out a multi-line comment.

Couple of points:

  • In perl an empty string ("") tests false.
  • /^\s*#/ will match a line beginning with zero or more whitespace then a #.
  • Unless chomped, input lines will have a newline at the end.

Here's the output from John1024's example:

#
# Some comments
#

sp_after_tparen_close      = ignore   # ignore/add/remove/force # Add or remove between the parens in the function type: 'void (*x)(...)'

some code #a comment line
more code

# comment one
# comment two
# comment three
still more code

Notice where # comment three is here in relation to John1024 and mikeserv's output. This is intentional, but the value of that depends on your requirements -- here my preference would be to assume that the last line of a multi-line comment should not be prepended to the next line of code, i.e., only single line comments followed by code are moved. Accomplishing this added complexity to the script; the $linesBuffered variable and logic relating to it would not be necessary otherwise.

5
  • I disagree with the one-liner biz...
    – mikeserv
    Oct 20, 2014 at 22:55
  • Waaaay beyond me at this point :-) Oct 20, 2014 at 23:01
  • 1
    @mikeserv It is just an opinion, I just can't be bothered prefacing everything with "IMO", especially when it is the correct opinion anyway ;) (I think) the significant difference here is the way multi-line comments get handled; in yours # comment 3 gets appended whereas this will not do that on purpose, but that requires a more complex state machine. For me it is very quick to do this kind of thing and easy to tweak to accommodate unforeseen requirements because it is lower level than one big regexp.
    – goldilocks
    Oct 21, 2014 at 4:22
  • 1
    ...Added my output for John's input to illustrate what I mean.
    – goldilocks
    Oct 21, 2014 at 4:28
  • agreed. that is better, I think. I could do it, too, with sed with an additional s/// or two, I think - maybe if two comment lines are found one after the other doubling the hashes and then not appending doubly-hashed comments.... but I'm too tired right now. Think I'll just go back to sleep...
    – mikeserv
    Oct 21, 2014 at 12:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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