summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/doc-docbook/spec.xfpt31
-rw-r--r--doc/doc-txt/ChangeLog2
-rw-r--r--src/src/expand.c141
-rw-r--r--test/scripts/0000-Basic/00027
-rw-r--r--test/stdout/00027
5 files changed, 188 insertions, 0 deletions
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'