split text file according to brackets or parantheses (top-level only) in terminal

Viewed 539

I have several text files (utf-8) that I want to process in shell script. They aren't excactly the same format, but if I could only break them up into edible chunks I can handle that. This could be programmed in C or python, but I prefer not.

EDIT: I wrote a solution in C; see my own answer. I think this may be the simplest approach after all. If you think I'm wrong please test your solution against the more complicated example input from my answer below.

-- jcxz100

For clarity (and to be able to debug more easily) I want the chunks to be saved as separate text files in a sub-folder.

All types of input files consist of:

  1. junk lines
  2. lines with junk text followed by start brackets or parentheses - i.e. '[' '{' '<' or '(' - and possibly followed by payload
  3. payload lines
  4. lines with brackets or parentheses nested within the top-level pairs; treated as payload too
  5. payload lines with end brackets or parantheses - i.e. ']' '}' '>' or ')' - possibly followed by something (junk text and/or start of a new payload)

I want to break up the input according to only the matching pairs of top-level brackets/parantheses. Payload inside these pairs must not be altered (including newlines and whitespace). Everything outside the toplevel pairs should be discarded as junk.

Any junk or payload inside double-quotes must be considered atomic (handled as raw text, thus any brackets or parentheses inside should also be treated as text).

Here is an example (using only {} pairs):

junk text
"atomic junk"

some junk text followed by a start bracket { here is the actual payload
   more payload
   "atomic payload"
   nested start bracket { - all of this line is untouchable payload too
      here is more payload
      "yet more atomic payload; this one's got a smiley ;-)"
   end of nested bracket pair } - all of this line is untouchable payload too
   this is payload too
} trailing junk
intermittent junk
{
   payload that goes in second output file    }
end junk

...sorry: Some of the input files really are as messy as that.

The first output file should be:

{ here is the actual payload
   more payload
   "atomic payload"
   nested start bracket { - all of this line is untouchable payload too
      here is more payload
      "yet more atomic payload; this one's got a smiley ;-)"
   end of nested bracket pair } - all of this line is untouchable payload too
   this is payload too
}

... and the second output file:

{
   payload that goes in second output file    }

Note:

  • I haven't quite decided whether it's necesary to keep the pair of start/end characters in the output or if they themselves should be discarded as junk. I think a solution that keeps them in is more general use.

  • There can be a mix of types of top-level bracket/paranthesis pairs in the same input file.

  • Beware: There are * and $ characters in the input files, so please avoid confusing bash ;-)

  • I prefer readability over brevity; but not at an exponential cost of speed.

Nice-to-haves:

  • There are backslash-escaped double-quotes inside the text; preferably they should be handled (I have a hack, but it's not pretty).

  • The script oughtn't break over mismatched pairs of brackets/parentheses in junk and/or payload (note: inside the atomics they must be allowed!)

More-far-out-nice-to-haves:

  • I haven't seen it yet, but one could speculate that some input might have single-quotes rather than double-quotes to denote atomic content... or even a mix of both.

  • It would be nice if the script could be easily modified to parse input of similar structure but with different start/end characters or strings.

I can see this is quite a mouthful, but I think it wouldn't give a robust solution if I broke it down into simpler questions.

The main problem is splitting up the input correctly - everything else can be ignored or "solved" with hacks, so feel free to ignore the nice-to-haves and the more-far-out-nice-to-haves.

2 Answers

Given:

$ cat file
junk text
"atomic junk"

some junk text followed by a start bracket { here is the actual payload
   more payload
   "atomic payload"
   nested start bracket { - all of this line is untouchable payload too
      here is more payload
      "yet more atomic payload; this one's got a smiley ;-)"
   end of nested bracket pair } - all of this line is untouchable payload too
   this is payload too
} trailing junk
intermittent junk
{
   payload that goes in second output file    }
end junk

This perl file will extract the blocks you describe into files block_1, block_2, etc:

#!/usr/bin/perl
use v5.10;
use warnings;
use strict;

use Text::Balanced qw(extract_multiple extract_bracketed);

my $txt;

while (<>){$txt.=$_;}  # slurp the file

my @blocks = extract_multiple(
    $txt,
    [
        # Extract {...}
        sub { extract_bracketed($_[0], '{}') },
    ],
    # Return all the fields
    undef,
    # Throw out anything which does not match
    1
);
chdir "/tmp";
my $base="block_";
my $cnt=1;
for my $block (@blocks){ my $fn="$base$cnt";
                         say "writing $fn";
                         open (my $fh, '>', $fn) or die "Could not open file '$fn' $!";
                         print $fh "$block\n";
                         close $fh;
                         $cnt++;}

Now the files:

$ cat block_1
{ here is the actual payload
   more payload
   "atomic payload"
   nested start bracket { - all of this line is untouchable payload too
      here is more payload
      "yet more atomic payload; this one's got a smiley ;-)"
   end of nested bracket pair } - all of this line is untouchable payload too
   this is payload too
}

$ cat block_2
{
   payload that goes in second output file    }

Using Text::Balanced is robust and likely the best solution.

You can do the blocks with a single Perl regex:

$ perl -0777 -nlE 'while (/(\{(?:(?1)|[^{}]*+)++\})|[^{}\s]++/g) {if ($1) {$cnt++; say "block $cnt:== start:\n$1\n== end";}}' file
block 1:== start:
{ here is the actual payload
   more payload
   "atomic payload"
   nested start bracket { - all of this line is untouchable payload too
      here is more payload
      "yet more atomic payload; this one's got a smiley ;-)"
   end of nested bracket pair } - all of this line is untouchable payload too
   this is payload too
}
== end
block 2:== start:
{
   payload that goes in second output file    }
== end

But that is a little more fragile than using a proper parser like Text::Balanced...

I have a solution in C. It would seem there's too much complexity for this to be easily achieved in shell script. The program isn't overly complicated but nevertheless has more than 200 lines of code, which include error checking, some speed optimization, and other niceties.

Source file split-brackets-to-chunks.c:

#include <stdio.h>

/* Example code by jcxz100 - your problem if you use it! */

#define BUFF_IN_MAX 255
#define BUFF_IN_SIZE (BUFF_IN_MAX+1)

#define OUT_NAME_MAX 31
#define OUT_NAME_SIZE (OUT_NAME_MAX+1)

#define NO_CHAR '\0'

int main()
{
    char pcBuff[BUFF_IN_SIZE];
    size_t iReadActual;
    FILE *pFileIn, *pFileOut;
    int iNumberOfOutputFiles;
    char pszOutName[OUT_NAME_SIZE];
    char cLiteralChar, cAtomicChar, cChunkStartChar, cChunkEndChar;
    int iChunkNesting;
    char *pcOutputStart;
    size_t iOutputLen;

    pcBuff[BUFF_IN_MAX] = '\0';  /* ... just to be sure. */
    iReadActual = 0;
    pFileIn = pFileOut = NULL;
    iNumberOfOutputFiles = 0;
    pszOutName[OUT_NAME_MAX] = '\0';  /* ... just to be sure. */
    cLiteralChar = cAtomicChar = cChunkStartChar = cChunkEndChar = NO_CHAR;
    iChunkNesting = 0;
    pcOutputStart = (char*)pcBuff;
    iOutputLen = 0;

    if ((pFileIn = fopen("input-utf-8.txt", "r")) == NULL)
    {
        printf("What? Where?\n");
        return 1;
    }

    while ((iReadActual = fread(pcBuff, sizeof(char), BUFF_IN_MAX, pFileIn)) > 0)
    {
        char *pcPivot, *pcStop;

        pcBuff[iReadActual] = '\0'; /* ... just to be sure. */
        pcPivot = (char*)pcBuff;
        pcStop = (char*)pcBuff + iReadActual;

        while (pcPivot < pcStop)
        {
            if (cLiteralChar != NO_CHAR) /* Ignore this char? */
            {
                /* Yes, ignore this char. */

                if (cChunkStartChar != NO_CHAR)
                {
                    /* ... just write it out: */
                    fprintf(pFileOut, "%c", *pcPivot);
                }
                pcPivot++;
                cLiteralChar = NO_CHAR;

                /* End of "Yes, ignore this char." */
            }
            else if (cAtomicChar != NO_CHAR) /* Are we inside an atomic string? */
            {
                /* Yup; we are inside an atomic string. */

                int bBreakInnerWhile;
                bBreakInnerWhile = 0;

                pcOutputStart = pcPivot;
                while (bBreakInnerWhile == 0)
                {
                    if (*pcPivot == '\\') /* Treat next char as literal? */
                    {
                        cLiteralChar = '\\'; /* Yes. */
                        bBreakInnerWhile = 1;
                    }
                    else if (*pcPivot == cAtomicChar) /* End of atomic? */
                    {
                        cAtomicChar = NO_CHAR; /* Yes. */
                        bBreakInnerWhile = 1;
                    }
                    if (++pcPivot == pcStop) bBreakInnerWhile = 1;
                }
                if (cChunkStartChar != NO_CHAR)
                {
                    /* The atomic string is part of a chunk. */
                    iOutputLen = (size_t)(pcPivot-pcOutputStart);
                    fprintf(pFileOut, "%.*s", iOutputLen, pcOutputStart);
                }

                /* End of "Yup; we are inside an atomic string." */
            }
            else if (cChunkStartChar == NO_CHAR) /* Are we inside a chunk? */
            {
                /* No, we are outside a chunk. */

                int bBreakInnerWhile;
                bBreakInnerWhile = 0;
                while (bBreakInnerWhile == 0)
                {
                    /* Detect start of anything interesting: */
                    switch (*pcPivot)
                    {
                        /* Start of atomic? */
                        case '"':
                        case '\'':
                            cAtomicChar = *pcPivot;
                            bBreakInnerWhile = 1;
                            break;

                        /* Start of chunk? */
                        case '{':
                            cChunkStartChar = *pcPivot;
                            cChunkEndChar = '}';
                            break;
                        case '[':
                            cChunkStartChar = *pcPivot;
                            cChunkEndChar = ']';
                            break;
                        case '(':
                            cChunkStartChar = *pcPivot;
                            cChunkEndChar = ')';
                            break;
                        case '<':
                            cChunkStartChar = *pcPivot;
                            cChunkEndChar = '>';
                            break;
                    }
                    if (cChunkStartChar != NO_CHAR)
                    {
                        iNumberOfOutputFiles++;
                        printf("Start '%c' '%c' chunk (file %04d.txt)\n", *pcPivot, cChunkEndChar, iNumberOfOutputFiles);
                        sprintf((char*)pszOutName, "output/%04d.txt", iNumberOfOutputFiles);
                        if ((pFileOut = fopen(pszOutName, "w")) == NULL)
                        {
                            printf("What? How?\n");
                            fclose(pFileIn);
                            return 2;
                        }
                        bBreakInnerWhile = 1;
                    }
                    else if (++pcPivot == pcStop)
                    {
                        bBreakInnerWhile = 1;
                    }
                }

                /* End of "No, we are outside a chunk." */
            }
            else
            {
                /* Yes, we are inside a chunk. */

                int bBreakInnerWhile;
                bBreakInnerWhile = 0;

                pcOutputStart = pcPivot;
                while (bBreakInnerWhile == 0)
                {
                    if (*pcPivot == cChunkStartChar)
                    {
                        /* Increase level of brackets/parantheses: */
                        iChunkNesting++;
                    }
                    else if (*pcPivot == cChunkEndChar)
                    {
                        /* Decrease level of brackets/parantheses: */
                        iChunkNesting--;
                        if (iChunkNesting == 0)
                        {
                            /* We are now outside chunk. */
                            bBreakInnerWhile = 1;
                        }
                    }
                    else
                    {
                        /* Detect atomic start: */
                        switch (*pcPivot)
                        {
                            case '"':
                            case '\'':
                                cAtomicChar = *pcPivot;
                                bBreakInnerWhile = 1;
                                break;
                        }
                    }
                    if (++pcPivot == pcStop) bBreakInnerWhile = 1;
                }
                iOutputLen = (size_t)(pcPivot-pcOutputStart);
                fprintf(pFileOut, "%.*s", iOutputLen, pcOutputStart);
                if (iChunkNesting == 0)
                {
                    printf("File done.\n");
                    cChunkStartChar = cChunkEndChar = NO_CHAR;
                    fclose(pFileOut);
                    pFileOut = NULL;
                }

                /* End of "Yes, we are inside a chunk." */
            }
        }
    }
    if (cChunkStartChar != NO_CHAR)
    {
        printf("Chunk exceeds end-of-file. Exiting gracefully.\n");
        fclose(pFileOut);
        pFileOut = NULL;
    }

    if (iNumberOfOutputFiles == 0) printf("Nothing to do...\n");
    else printf("All done.\n");
    fclose(pFileIn);
    return 0;
}

I've solved the nice-to-haves and one of the more-far-out-nice-to-haves. To show this the input is a little more complex than the example in the question:

junk text
"atomic junk"

some junk text followed by a start bracket { here is the actual payload
   more payload
   'atomic payload { with start bracket that should be ignored'
   nested start bracket { - all of this line is untouchable payload too
      here is more payload
"this atomic has a literal double-quote \" inside"
      "yet more atomic payload; this one's got a smiley ;-) and a heart <3"
   end of nested bracket pair } - all of this line is untouchable payload too
   this is payload too
   "here's a totally unprovoked $ sign and an * asterisk"
} trailing junk
intermittent junk
<
   payload that goes in second output file } mismatched end bracket should be ignored     >
end junk

Resulting file output/0001.txt:

{ here is the actual payload
   more payload
   'atomic payload { with start bracket that should be ignored'
   nested start bracket { - all of this line is untouchable payload too
      here is more payload
"this atomic has a literal double-quote \" inside"
      "yet more atomic payload; this one's got a smiley ;-) and a heart <3"
   end of nested bracket pair } - all of this line is untouchable payload too
   this is payload too
   "here's a totally unprovoked $ sign and an * asterisk"
}

... and resulting file output/0002.txt:

<
   payload that goes in second output file } mismatched end bracket should be ignored     >

Thanks @dawg for your help :)

Related