2

I've got a set of POSIX regular expressions*

^BEGIN:VCARD\r$
^VERSION[^A-Z]
^FN[^A-Z]
^N[^A-Z]
^NICKNAME[^A-Z]
^EMAIL[^A-Z]
^X-\([A-Z-]*\)
^TEL[^A-Z]
^ADR[^A-Z]
^ORG[^A-Z]
^TITLE[^A-Z]
^BDAY[^A-Z]
^URL[^A-Z]
^ROLE[^A-Z]
^NOTE[^A-Z]
^END:VCARD\r$

and a file with lines which each match one of the regular expressions:

BEGIN:VCARD
VERSION:3.0
N:Doe;Jane;;Ms;
URL:http://janedoe.com/
EMAIL:jdoe@example.org
EMAIL:jane.doe@janedoe.com
BDAY:1970-01-01
X-JABBER:jane.doe@example.org
X-ICQ:1234567890
END:VCARD

I'd like to sort these lines according to

  1. the line number of the regex match (so that lines starting with FN comes before lines starting with N),
  2. the match group (so that X-ABC comes before X-DEF)

Ideally, the other parts of the lines should not be sorted (so the sequence of lines which start with EMAIL should be left alone). The expected result should therefore be:

BEGIN:VCARD
VERSION:3.0
N:Doe;Jane;;Ms;
EMAIL:jdoe@example.org
EMAIL:jane.doe@janedoe.com
X-ICQ:1234567890
X-JABBER:jane.doe@example.org
BDAY:1970-01-01
URL:http://janedoe.com/
END:VCARD

Is there an existing tool to do this?

Edit: Resulting implementation based on Lars Rohrbach's answer.

* This is the sequence of vCard properties in a Gmail contacts export file.

  • With just about any tool, you'll likely have a problem with the unmatched 'remaining' lines since there will be nothing relative to indicate their position. In your example, you are not keeping their position relative to the original (which should be after X-ICQ) or relative to the lines preceding or succeeding being sorted. Otherwise, the lines would be after URL or before BDAY. – Arcege Jun 11 '12 at 1:42
  • This sounds a bit like meta-regex-golf... xkcd.com/1313 – Fabian Röling Nov 27 '19 at 11:50
3

The usual sort command doesn't provide an included way to specify your specific "dictionary", and while the grep command allows you to provide a file of regular expressions, it won't change the order of the output. But you can put both together in a simple foreach loop -- here's an example that works in the bash shell:

for i in `cat fileofregexp`; do grep "$i" myinputfile; done

This takes each regexp line from your file of regular expressions one by one, and outputs any match from your inputfile, so the resulting output will be sorted by your regexp order. Note that any lines in your inputfile that don't match at all will not make it to the output.

Addendum: As requested, here's a version using a while loop:

while IFS= read -r i; do grep "$i" myinputfile; done  < fileofregexp
1

This isn't exactly how you framed it, but given your actual purpose it would be simpler to grab the part before the colon and sort by that. Here's a Perl script which accumulates lines from separate sort keys in separate array entries, and flushes a vcard when it reaches the end.

#!/usr/bin/perl -n
BEGIN {
    @headers = qw(BEGIN VERSION FN N NICKNAME EMAIL X- TEL ADR ORG TITLE BDAY URL ROLE NOTE END);
    for $h (@headers) { $data{$h} = ""; }
}
if (/^([^:]+):/) {
    $data{exists $data{$1} ? $1 : "X-"} .= $_;
    if ($1 eq 'END') {
        for $h (@headers) { print $data{$h}; $data{$h} = ""; }
    }
} else {
    print;
}

And if you really want the full regexp flexibility, iterate over regexps instead of looking up a key in a hash.

#!/usr/bin/perl -n
BEGIN {
    @regexps = qw(^BEGIN:VCARD\r$ ^VERSION[^A-Z] ^FN[^A-Z] ^N[^A-Z] ... ^END:VCARD\r$);
    for $r (@regexps) { $data{$r} = ""; }
}
for $r (@regexps) {
    next unless $_ =~ $r;
    $data{$r} .= $_;
    last;
}
if ($_ =~ $regexps[@regexps-1]) {
    for $r (@regexps) { print "++", $data{$r}; $data{$r} = ""; }
}

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.