Add expansion item for sorting lists
authorJeremy Harris <jgh146exb@wizmail.org>
Sat, 6 Sep 2014 20:10:17 +0000 (21:10 +0100)
committerJeremy Harris <jgh146exb@wizmail.org>
Sat, 6 Sep 2014 20:45:44 +0000 (21:45 +0100)
doc/doc-docbook/spec.xfpt
doc/doc-txt/ChangeLog
src/src/expand.c
test/scripts/0000-Basic/0002
test/stdout/0002

index 69009e92da44389e9527ad9874343ec3f34e5e9e..df63ad6a0bc5f05a208e198176b24ab9820a8204 100644 (file)
@@ -9698,6 +9698,37 @@ the regular expression from string expansion.
 
 
 
+.new
+.vitem &*${sort{*&<&'string'&>&*}{*&<&'comparator'&>&*}{*&<&'extractor'&>&*}}*&
+.cindex sorting a list
+.cindex list sorting
+After expansion, <&'string'&> is interpreted as a list, colon-separated by
+default, but the separator can be changed in the usual way.
+The <&'comparator'&> argument is interpreted as the operator
+of a two-argument expansion condition.
+The numeric operators plus ge, gt, le, lt (and ~i variants) are supported.
+The comparison should return true when applied to two values
+if the first value should sort before the second value.
+The <&'extractor'&> expansion is applied repeatedly to elements of the list,
+the element being placed in &$item$&,
+to give values for comparison.
+
+The item result is a sorted list,
+with the original list separator,
+of the list elements (in full) of the original.
+
+Examples:
+.code
+${sort{3:2:1:4}{<}{$item}}
+.endd
+sorts a list of numbers, and
+.code
+${sort {$lookup dnsdb{>:,,mx=example.com}} {<} {${listextract{1}{<,$item}}}}
+.endd
+will sort an MX lookup into priority order.
+.wen
+
+
 .vitem &*${substr{*&<&'string1'&>&*}{*&<&'string2'&>&*}{*&<&'string3'&>&*}}*&
 .cindex "&%substr%& expansion item"
 .cindex "substring extraction"
index c8e6ccf2ba2da54809fdafd421d82e8657bf9118..49211bc5661ac08528cc0f186248797cd76b334a 100644 (file)
@@ -30,6 +30,8 @@ JH/02 Add EXPERIMENTAL_DANE, allowing for using the DNS as trust-anchor for
 
 JH/03 Support secondary-separator specifier for MX, SRV, TLSA lookups.
 
+JH/04 Add ${sort {list}{condition}{extractor}} expansion item.
+
 
 Exim version 4.84
 -----------------
index a929e937c05c8fe5175406e481998675322cca03..8e94c3e4b67edbe1ddb57e6e294815aecf1f96aa 100644 (file)
@@ -127,6 +127,7 @@ static uschar *item_table[] = {
   US"reduce",
   US"run",
   US"sg",
+  US"sort",
   US"substr",
   US"tr" };
 
@@ -152,6 +153,7 @@ enum {
   EITEM_REDUCE,
   EITEM_RUN,
   EITEM_SG,
+  EITEM_SORT,
   EITEM_SUBSTR,
   EITEM_TR };
 
@@ -5633,6 +5635,145 @@ while (*s != 0)
       continue;
       }
 
+    case EITEM_SORT:
+      {
+      int sep = 0;
+      uschar *srclist, *cmp, *xtract;
+      uschar *srcitem;
+      uschar *dstlist = NULL;
+      uschar *dstkeylist = NULL;
+      uschar * tmp;
+      uschar *save_iterate_item = iterate_item;
+
+      while (isspace(*s)) s++;
+      if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+
+      srclist = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok);
+      if (!srclist) goto EXPAND_FAILED;
+      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+
+      while (isspace(*s)) s++;
+      if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+
+      cmp = expand_string_internal(s, TRUE, &s, skipping, FALSE, &resetok);
+      if (!cmp) goto EXPAND_FAILED;
+      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+
+      while (isspace(*s)) s++;
+      if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+
+      xtract = s;
+      tmp = expand_string_internal(s, TRUE, &s, TRUE, TRUE, &resetok);
+      if (!tmp) goto EXPAND_FAILED;
+      xtract = string_copyn(xtract, s - xtract);
+
+      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+                                                       /*{*/
+      if (*s++ != '}')
+        {                                              /*{*/
+        expand_string_message = US"missing } at end of \"sort\"";
+        goto EXPAND_FAILED;
+        }
+
+      if (skipping) continue;
+
+      while ((srcitem = string_nextinlist(&srclist, &sep, NULL, 0)))
+        {
+       uschar * dstitem;
+       uschar * newlist = NULL;
+       uschar * newkeylist = NULL;
+       uschar * srcfield;
+
+        DEBUG(D_expand) debug_printf("%s: $item = \"%s\"\n", name, srcitem);
+
+       /* extract field for comparisons */
+       iterate_item = srcitem;
+       if (  !(srcfield = expand_string_internal(xtract, FALSE, NULL, FALSE,
+                                         TRUE, &resetok))
+          || !*srcfield)
+         {
+         expand_string_message = string_sprintf(
+             "field-extract in sort: \"%s\"", xtract);
+         goto EXPAND_FAILED;
+         }
+
+       /* Insertion sort */
+
+       /* copy output list until new-item < list-item */
+       while ((dstitem = string_nextinlist(&dstlist, &sep, NULL, 0)))
+         {
+         uschar * dstfield;
+         uschar * expr;
+         BOOL before;
+
+         /* field for comparison */
+         if (!(dstfield = string_nextinlist(&dstkeylist, &sep, NULL, 0)))
+           goto sort_mismatch;
+
+         /* build and run condition string */
+         expr = string_sprintf("%s{%s}{%s}", cmp, srcfield, dstfield);
+
+         DEBUG(D_expand) debug_printf("%s: cond = \"%s\"\n", name, expr);
+         if (!eval_condition(expr, &resetok, &before))
+           {
+           expand_string_message = string_sprintf("comparison in sort: %s",
+               expr);
+           goto EXPAND_FAILED;
+           }
+
+         if (before)
+           {
+           /* New-item sorts before this dst-item.  Append new-item,
+           then dst-item, then remainder of dst list. */
+
+           newlist = string_append_listele(newlist, sep, srcitem);
+           newkeylist = string_append_listele(newkeylist, sep, srcfield);
+           srcitem = NULL;
+
+           newlist = string_append_listele(newlist, sep, dstitem);
+           newkeylist = string_append_listele(newkeylist, sep, dstfield);
+
+           while ((dstitem = string_nextinlist(&dstlist, &sep, NULL, 0)))
+             {
+             if (!(dstfield = string_nextinlist(&dstkeylist, &sep, NULL, 0)))
+               goto sort_mismatch;
+             newlist = string_append_listele(newlist, sep, dstitem);
+             newkeylist = string_append_listele(newkeylist, sep, dstfield);
+             }
+
+           break;
+           }
+
+         newlist = string_append_listele(newlist, sep, dstitem);
+         newkeylist = string_append_listele(newkeylist, sep, dstfield);
+         }
+
+       /* If we ran out of dstlist without consuming srcitem, append it */
+       if (srcitem)
+         {
+         newlist = string_append_listele(newlist, sep, srcitem);
+         newkeylist = string_append_listele(newkeylist, sep, srcfield);
+         }
+
+       dstlist = newlist;
+       dstkeylist = newkeylist;
+
+        DEBUG(D_expand) debug_printf("%s: dstlist = \"%s\"\n", name, dstlist);
+        DEBUG(D_expand) debug_printf("%s: dstkeylist = \"%s\"\n", name, dstkeylist);
+       }
+
+      if (dstlist)
+       yield = string_cat(yield, &size, &ptr, dstlist, Ustrlen(dstlist));
+
+      /* Restore preserved $item */
+      iterate_item = save_iterate_item;
+      continue;
+
+      sort_mismatch:
+       expand_string_message = US"Internal error in sort (list mismatch)";
+       goto EXPAND_FAILED;
+      }
+
 
     /* If ${dlfunc } support is configured, handle calling dynamically-loaded
     functions, unless locked out at this time. Syntax is ${dlfunc{file}{func}}
index 51dc6ae48661185680e54f259f073a5699bca74d..bcfacdea4ca9be4b14626bb47049502fe9577418 100644 (file)
@@ -82,6 +82,13 @@ listextract: ${listextract{-5}{a:b:c:d}}
 listextract: ${listextract{ 5}{a:b:c:d}{}{fail}}
 listextract: ${listextract{ 5}{a:b:c:d}{}fail}
 
+sort: ${sort{3:2:1:4}{<}{$item}}
+sort: ${sort {<, 3,2,1,4}{>}{$item}}
+sort: ${sort{c:B:a:aa}{lti}{$item}}
+sort: ${sort{666 r99.ex.com:10 smtp.ex.com:100 r2.ex.com}{<}{${sg {$item}{([0-9]*).*\$}{\$1}}}}
+sort: ${sort{666,r99.ex.com:10,smtp.ex.com:100,r2.ex.com}{<}{${listextract{1}{<,$item}}}}
+sort: "${sort{}{<}{$item}}"
+
 # Tests with iscntrl() and illegal separators
 
 map: ${map{<\n a\n\nb\nc}{'$item'}}
index 7200bf3a78211242dec6167423f42de127cf4aeb..a3706ee5ae844aebb4a9c1fc6ea45e86e19cb7a6 100644 (file)
 > listextract: fail
 > Failed: "extract" failed and "fail" requested
 > 
+> sort: 1:2:3:4
+> sort: 4,3,2,1
+> sort: a:aa:B:c
+> sort: 10 smtp.ex.com:100 r2.ex.com:666 r99.ex.com
+> sort: 10,smtp.ex.com:100,r2.ex.com:666,r99.ex.com
+> sort: ""
+> 
 > # Tests with iscntrl() and illegal separators
 > 
 > map: 'a'