Search code examples
bashfor-loopsedvariable-substitution

How to find the nth multiline block of text using sed


So I have a file which contains blocks that looks as follows:

menuentry ... {
....
....
}

....

menuentry ... {
....
....
}

I need to look at the contents of each menu entry in a bash script. I have very limited experience with sed but through a very exhaustive search I was able to construct the following:

cat $file | sed '/^menuentry.*{/!d;x;s/^/x/;/x\{1\}/!{x;d};x;:a;n;/^}/!ba;q'

and I can replace the \{1\} with whatever number I want to get the nth block. This works fine like that, but the problem is I need to iterate through an arbitrary number of times:

numEntries=$( egrep -c "^menuentry.*{" $file ) 
for i in $( seq 1 $numEntries); do

    i=$( echo $i | tr -d '\n' ) # A google search indicated sed  encounters problems
                                # when a substituted variable has a trailing return char

    # Get the nth entry block of text with the sed statement from before, 
    # but replace with variable $i
    entry=$( cat $file | sed '/^menuentry.*{/!d;x;s/^/x/;/x\{$i\}/!{x;d};x;:a;n;/^}/!ba;q')

    # Do some stuff with $entry #

done

I've tried with every combination of quotes/double quotes and braces around the variable and around the sed statement and every which way I do it, I get some sort of error. As I said, I don't really know much about sed and that frankenstein of a statement is just what I managed to mish-mosh together from various google searches, so any help or explanation would be much appreciated!

TIA


Solution

  • sed is for simple substitutions on individual lines, that is all, and shell loops to manipulate text are immensely slow and difficult to write robustly. The folks who invented sed and shell also invented awk for tasks like this:

    awk -v RS= 'NR==3' file
    

    would print the 3rd blank-line-separated block of text as shown in your question. This would print every block containing the string "foobar":

    awk -v RS= '/foobar/' file
    

    and anything else you might want to do is equally trivial.

    The above will work efficiently, robustly and portably using any awk in any shell on any UNIX box. For example:

    $ cat file
    menuentry first {
        Now is the Winter
        of our discontent
    }
    
    menuentry second {
        Wee sleekit cowrin
        timrous beastie,
        oh whit a panic's in
        thy breastie.
    }
    
    menuentry third {
        Twas the best of times
        Twas the worst of times
        Make up your damn mind
    }
    

    .

    $ awk -v RS= 'NR==3' file
    menuentry third {
        Twas the best of times
        Twas the worst of times
        Make up your damn mind
    }
    
    $ awk -v RS= 'NR==2' file
    menuentry second {
        Wee sleekit cowrin
        timrous beastie,
        oh whit a panic's in
        thy breastie.
    }
    
    $ awk -v RS= '/beastie/' file
    menuentry second {
        Wee sleekit cowrin
        timrous beastie,
        oh whit a panic's in
        they breastie.
    }
    

    If you find yourself trying to do anything other than s/old/new with sed and/or using sed commands other than s, g and p (with -n) then you are using constructs that became obsolete in the mid-1970s when awk was invented.

    If the above doesn't work for you then edit your question to provide more truly representative sample input and expected output.