From ac4ef9bdcad6bb41c919f6d0b76fdef717ab5e18 Mon Sep 17 00:00:00 2001 From: Jeremy Harris Date: Sat, 6 Sep 2014 21:10:17 +0100 Subject: [PATCH] Add expansion item for sorting lists --- doc/doc-docbook/spec.xfpt | 31 ++++++++ doc/doc-txt/ChangeLog | 2 + src/src/expand.c | 141 +++++++++++++++++++++++++++++++++++ test/scripts/0000-Basic/0002 | 7 ++ test/stdout/0002 | 7 ++ 5 files changed, 188 insertions(+) diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt index 69009e92d..df63ad6a0 100644 --- a/doc/doc-docbook/spec.xfpt +++ b/doc/doc-docbook/spec.xfpt @@ -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" diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog index c8e6ccf2b..49211bc56 100644 --- a/doc/doc-txt/ChangeLog +++ b/doc/doc-txt/ChangeLog @@ -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 ----------------- diff --git a/src/src/expand.c b/src/src/expand.c index a929e937c..8e94c3e4b 100644 --- a/src/src/expand.c +++ b/src/src/expand.c @@ -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}} diff --git a/test/scripts/0000-Basic/0002 b/test/scripts/0000-Basic/0002 index 51dc6ae48..bcfacdea4 100644 --- a/test/scripts/0000-Basic/0002 +++ b/test/scripts/0000-Basic/0002 @@ -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'}} diff --git a/test/stdout/0002 b/test/stdout/0002 index 7200bf3a7..a3706ee5a 100644 --- a/test/stdout/0002 +++ b/test/stdout/0002 @@ -71,6 +71,13 @@ > 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' -- 2.30.2