This is an automated email from the ASF dual-hosted git repository.
pvillard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 5854916aa39 NIFI-15649 Add compactDelimitedList() and
trimDelimitedList() Expression Language functions
5854916aa39 is described below
commit 5854916aa393fc8fa948b0c2fe4cb4d29d3f661d
Author: mcducks <[email protected]>
AuthorDate: Thu Feb 26 22:13:44 2026 +1100
NIFI-15649 Add compactDelimitedList() and trimDelimitedList() Expression
Language functions
* compactDelimitedList removes all empty tokens from delimited strings
* trimDelimitedList removes only leading/trailing empty tokens, preserving
interior empties
* Handles multi-character delimiters
* Added unit tests in TestQuery
* Updated ANTLR grammar (lexer and parser)
* Integrated into ExpressionCompiler
* Added documentation to expression language guide
This closes #10938.
Signed-off-by: Pierre Villard <[email protected]>
---
.../language/antlr/AttributeExpressionLexer.g | 2 +
.../language/antlr/AttributeExpressionParser.g | 2 +-
.../language/compile/ExpressionCompiler.java | 16 +++
.../functions/CompactDelimitedListEvaluator.java | 80 +++++++++++++++
.../functions/TrimDelimitedListEvaluator.java | 94 ++++++++++++++++++
.../attribute/expression/language/TestQuery.java | 110 +++++++++++++++++++++
.../main/asciidoc/expression-language-guide.adoc | 68 +++++++++++++
7 files changed, 371 insertions(+), 1 deletion(-)
diff --git
a/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g
b/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g
index 98c498988d3..747f907cf2a 100644
---
a/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g
+++
b/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g
@@ -220,6 +220,8 @@ UUID3 : 'UUID3';
UUID5 : 'UUID5';
HASH : 'hash';
UNIQUE : 'unique';
+COMPACT_DELIMITED_LIST : 'compactDelimitedList';
+TRIM_DELIMITED_LIST : 'trimDelimitedList';
// 2 arg functions
SUBSTRING : 'substring';
diff --git
a/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionParser.g
b/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionParser.g
index bb4942124ba..aaf69b62c90 100644
---
a/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionParser.g
+++
b/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionParser.g
@@ -77,7 +77,7 @@ tokens {
// functions that return Strings
zeroArgString : (TO_UPPER | TO_LOWER | TRIM | TO_STRING | URL_ENCODE |
URL_DECODE | BASE64_ENCODE | BASE64_DECODE | ESCAPE_JSON | ESCAPE_XML |
ESCAPE_CSV | ESCAPE_HTML3 | ESCAPE_HTML4 | UNESCAPE_JSON | UNESCAPE_XML |
UNESCAPE_CSV | UNESCAPE_HTML3 | UNESCAPE_HTML4 | EVALUATE_EL_STRING) LPAREN!
RPAREN!;
oneArgString : ((SUBSTRING_BEFORE | SUBSTRING_BEFORE_LAST | SUBSTRING_AFTER |
SUBSTRING_AFTER_LAST | REPLACE_NULL | REPLACE_EMPTY | REPLACE_BY_PATTERN |
- PREPEND | APPEND | STARTS_WITH | ENDS_WITH |
CONTAINS | UNIQUE | JOIN | JSON_PATH | JSON_PATH_DELETE | FROM_RADIX | UUID3 |
UUID5 | HASH) LPAREN! anyArg RPAREN!) |
+ PREPEND | APPEND | STARTS_WITH | ENDS_WITH |
CONTAINS | UNIQUE | COMPACT_DELIMITED_LIST | TRIM_DELIMITED_LIST | JOIN |
JSON_PATH | JSON_PATH_DELETE | FROM_RADIX | UUID3 | UUID5 | HASH) LPAREN!
anyArg RPAREN!) |
(TO_RADIX LPAREN! anyArg (COMMA! anyArg)? RPAREN!);
twoArgString : ((REPLACE | REPLACE_FIRST | REPLACE_ALL | IF_ELSE |
JSON_PATH_SET | JSON_PATH_ADD) LPAREN! anyArg COMMA! anyArg RPAREN!) |
((SUBSTRING | FORMAT | FORMAT_INSTANT | PAD_LEFT |
PAD_RIGHT | REPEAT) LPAREN! anyArg (COMMA! anyArg)? RPAREN!);
diff --git
a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java
b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java
index 4a7d85a9ad0..115a5d50cdb 100644
---
a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java
+++
b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java
@@ -48,6 +48,7 @@ import
org.apache.nifi.attribute.expression.language.evaluation.functions.Append
import
org.apache.nifi.attribute.expression.language.evaluation.functions.Base64DecodeEvaluator;
import
org.apache.nifi.attribute.expression.language.evaluation.functions.Base64EncodeEvaluator;
import
org.apache.nifi.attribute.expression.language.evaluation.functions.CharSequenceTranslatorEvaluator;
+import
org.apache.nifi.attribute.expression.language.evaluation.functions.CompactDelimitedListEvaluator;
import
org.apache.nifi.attribute.expression.language.evaluation.functions.ContainsEvaluator;
import
org.apache.nifi.attribute.expression.language.evaluation.functions.DivideEvaluator;
import
org.apache.nifi.attribute.expression.language.evaluation.functions.EndsWithEvaluator;
@@ -118,6 +119,7 @@ import
org.apache.nifi.attribute.expression.language.evaluation.functions.ToLowe
import
org.apache.nifi.attribute.expression.language.evaluation.functions.ToRadixEvaluator;
import
org.apache.nifi.attribute.expression.language.evaluation.functions.ToStringEvaluator;
import
org.apache.nifi.attribute.expression.language.evaluation.functions.ToUpperEvaluator;
+import
org.apache.nifi.attribute.expression.language.evaluation.functions.TrimDelimitedListEvaluator;
import
org.apache.nifi.attribute.expression.language.evaluation.functions.TrimEvaluator;
import
org.apache.nifi.attribute.expression.language.evaluation.functions.UniqueEvaluator;
import
org.apache.nifi.attribute.expression.language.evaluation.functions.UrlDecodeEvaluator;
@@ -172,6 +174,7 @@ import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpre
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.ATTR_NAME;
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.BASE64_DECODE;
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.BASE64_ENCODE;
+import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.COMPACT_DELIMITED_LIST;
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.CONTAINS;
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.COUNT;
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.DECIMAL;
@@ -258,6 +261,7 @@ import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpre
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.TO_STRING;
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.TO_UPPER;
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.TRIM;
+import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.TRIM_DELIMITED_LIST;
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.TRUE;
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.UNESCAPE_CSV;
import static
org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.UNESCAPE_HTML3;
@@ -1093,6 +1097,18 @@ public class ExpressionCompiler {
toStringEvaluator(subjectEvaluator),
toStringEvaluator(argEvaluators.get(0), "first
argument to unique")), "unique");
}
+ case COMPACT_DELIMITED_LIST: {
+ verifyArgCount(argEvaluators, 1, "compactDelimitedList");
+ return addToken(new CompactDelimitedListEvaluator(
+ toStringEvaluator(subjectEvaluator),
+ toStringEvaluator(argEvaluators.get(0), "first
argument to compactDelimitedList")), "compactDelimitedList");
+ }
+ case TRIM_DELIMITED_LIST: {
+ verifyArgCount(argEvaluators, 1, "trimDelimitedList");
+ return addToken(new TrimDelimitedListEvaluator(
+ toStringEvaluator(subjectEvaluator),
+ toStringEvaluator(argEvaluators.get(0), "first
argument to trimDelimitedList")), "trimDelimitedList");
+ }
default:
throw new
AttributeExpressionLanguageParsingException("Expected a Function-type
expression but got " + tree.toString());
}
diff --git
a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/CompactDelimitedListEvaluator.java
b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/CompactDelimitedListEvaluator.java
new file mode 100644
index 00000000000..935ff4ac56b
--- /dev/null
+++
b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/CompactDelimitedListEvaluator.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.attribute.expression.language.evaluation.functions;
+
+import org.apache.nifi.attribute.expression.language.EvaluationContext;
+import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
+import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
+import
org.apache.nifi.attribute.expression.language.evaluation.StringEvaluator;
+import
org.apache.nifi.attribute.expression.language.evaluation.StringQueryResult;
+
+import java.util.Arrays;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * <p>Removes all empty tokens from a delimited string, preserves the order of
+ * non-empty tokens, and rejoins them using the same delimiter.</p>
+ *
+ * <p>Examples with comma delimiter:</p>
+ * <ul>
+ * <li>,a,b,c,, becomes a,b,c</li>
+ * <li>a,,b,,,c becomes a,b,c</li>
+ * <li>,,, becomes empty string</li>
+ * </ul>
+ *
+ * <p>Usage: ${attribute:compactDelimitedList(',')}</p>
+ */
+public class CompactDelimitedListEvaluator extends StringEvaluator {
+
+ private final Evaluator<String> subject;
+ private final Evaluator<String> delimiter;
+
+ public CompactDelimitedListEvaluator(final Evaluator<String> subject,
+ final Evaluator<String> delimiter) {
+ this.subject = subject;
+ this.delimiter = delimiter;
+ }
+
+ @Override
+ public QueryResult<String> evaluate(final EvaluationContext
evaluationContext) {
+ final String subjectValue =
subject.evaluate(evaluationContext).getValue();
+ if (subjectValue == null) {
+ return new StringQueryResult("");
+ }
+
+ final String delimiterValue =
delimiter.evaluate(evaluationContext).getValue();
+ if (delimiterValue == null || delimiterValue.isEmpty()) {
+ return new StringQueryResult(subjectValue);
+ }
+
+ // Split on the delimiter, keeping trailing empties so we can remove
them
+ final String[] tokens =
subjectValue.split(Pattern.quote(delimiterValue), -1);
+
+ // Filter out all empty tokens and rejoin the rest
+ final String result = Arrays.stream(tokens)
+ .filter(token -> !token.isEmpty())
+ .collect(Collectors.joining(delimiterValue));
+
+ return new StringQueryResult(result);
+ }
+
+ @Override
+ public Evaluator<?> getSubjectEvaluator() {
+ return subject;
+ }
+}
diff --git
a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/TrimDelimitedListEvaluator.java
b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/TrimDelimitedListEvaluator.java
new file mode 100644
index 00000000000..842dcc7fdcb
--- /dev/null
+++
b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/TrimDelimitedListEvaluator.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.attribute.expression.language.evaluation.functions;
+
+import org.apache.nifi.attribute.expression.language.EvaluationContext;
+import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
+import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
+import
org.apache.nifi.attribute.expression.language.evaluation.StringEvaluator;
+import
org.apache.nifi.attribute.expression.language.evaluation.StringQueryResult;
+
+import java.util.Arrays;
+import java.util.regex.Pattern;
+
+/**
+ * Removes leading and trailing empty tokens from a delimited string while
+ * preserving any interior empty tokens. Non-empty token order is preserved
+ * and the result is rejoined using the same delimiter.
+ *
+ * <p>Examples with delimiter {@code ,}:
+ * <ul>
+ * <li>{@code ,a,b,c,,} becomes {@code a,b,c}</li>
+ * <li>{@code ,,a,,b,,} becomes {@code a,,b}</li>
+ * <li>{@code a,,b,,,c} is unchanged (no leading/trailing empties)</li>
+ * <li>{@code ,,,} becomes {@code ""} (empty string)</li>
+ * </ul>
+ *
+ * <p>Usage: {@code ${attribute:trimDelimitedList(',')}}
+ */
+public class TrimDelimitedListEvaluator extends StringEvaluator {
+
+ private final Evaluator<String> subject;
+ private final Evaluator<String> delimiter;
+
+ public TrimDelimitedListEvaluator(final Evaluator<String> subject,
+ final Evaluator<String> delimiter) {
+ this.subject = subject;
+ this.delimiter = delimiter;
+ }
+
+ @Override
+ public QueryResult<String> evaluate(final EvaluationContext
evaluationContext) {
+ final String subjectValue =
subject.evaluate(evaluationContext).getValue();
+ if (subjectValue == null) {
+ return new StringQueryResult("");
+ }
+
+ final String delimiterValue =
delimiter.evaluate(evaluationContext).getValue();
+ if (delimiterValue == null || delimiterValue.isEmpty()) {
+ return new StringQueryResult(subjectValue);
+ }
+
+ final String[] tokens =
subjectValue.split(Pattern.quote(delimiterValue), -1);
+
+ // Scan forward past leading empty tokens
+ int start = 0;
+ while (start < tokens.length && tokens[start].isEmpty()) {
+ start++;
+ }
+
+ // All tokens were empty, nothing to keep
+ if (start == tokens.length) {
+ return new StringQueryResult("");
+ }
+
+ // Scan backward past trailing empty tokens
+ int end = tokens.length - 1;
+ while (end > start && tokens[end].isEmpty()) {
+ end--;
+ }
+
+ // Rejoin the slice between start and end, preserving interior empties
+ return new StringQueryResult(
+ String.join(delimiterValue, Arrays.copyOfRange(tokens, start,
end + 1)));
+ }
+
+ @Override
+ public Evaluator<?> getSubjectEvaluator() {
+ return subject;
+ }
+}
diff --git
a/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
b/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
index 9e213e6456c..044b5ce962b 100644
---
a/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
+++
b/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
@@ -2756,4 +2756,114 @@ public class TestQuery {
attributes.put("dash_separated", "field1-field2-field1-field3");
verifyEquals("${dash_separated:unique('-')}", attributes,
"field1-field2-field3");
}
+
+ @Test
+ public void testCompactDelimitedList() {
+ final Map<String, String> attributes = new HashMap<>();
+
+ // Core behaviour — comma delimiter
+ attributes.put("input", ",a,b,c,,");
+ verifyEquals("${input:compactDelimitedList(',')}", attributes,
"a,b,c");
+
+ attributes.put("input", "a,,b,,,c");
+ verifyEquals("${input:compactDelimitedList(',')}", attributes,
"a,b,c");
+
+ attributes.put("input", ",,a,,b,,c,,");
+ verifyEquals("${input:compactDelimitedList(',')}", attributes,
"a,b,c");
+
+ attributes.put("input", ",,,");
+ verifyEquals("${input:compactDelimitedList(',')}", attributes, "");
+
+ attributes.put("input", "a,b,c");
+ verifyEquals("${input:compactDelimitedList(',')}", attributes,
"a,b,c");
+
+ attributes.put("input", "a");
+ verifyEquals("${input:compactDelimitedList(',')}", attributes, "a");
+
+ attributes.put("input", "");
+ verifyEquals("${input:compactDelimitedList(',')}", attributes, "");
+
+ attributes.put("input", ",");
+ verifyEquals("${input:compactDelimitedList(',')}", attributes, "");
+
+ // Pipe delimiter
+ attributes.put("input", "|a|b||c|");
+ verifyEquals("${input:compactDelimitedList('|')}", attributes,
"a|b|c");
+
+ // Multi-character delimiter
+ attributes.put("input", "::a::::b::c::");
+ verifyEquals("${input:compactDelimitedList('::')}", attributes,
"a::b::c");
+
+ // Regex-special character delimiter
+ attributes.put("input", "..a....b..c..");
+ verifyEquals("${input:compactDelimitedList('..')}", attributes,
"a..b..c");
+
+ // Whitespace-only tokens are non-empty and preserved
+ attributes.put("input", "a, ,b");
+ verifyEquals("${input:compactDelimitedList(',')}", attributes, "a,
,b");
+
+ // Chaining with trim() to also strip whitespace
+ attributes.put("input", " ,,a,,b,, ");
+ verifyEquals("${input:trim():compactDelimitedList(',')}", attributes,
"a,b");
+
+ // Missing attribute evaluates to empty string
+ verifyEquals("${missing:compactDelimitedList(',')}", attributes, "");
+ }
+
+ @Test
+ public void testTrimDelimitedList() {
+ final Map<String, String> attributes = new HashMap<>();
+
+ // Leading and trailing empties removed
+ attributes.put("input", ",a,b,c,,");
+ verifyEquals("${input:trimDelimitedList(',')}", attributes, "a,b,c");
+
+ // Interior empties preserved — key difference from
compactDelimitedList
+ attributes.put("input", "a,,b,,,c");
+ verifyEquals("${input:trimDelimitedList(',')}", attributes,
"a,,b,,,c");
+
+ attributes.put("input", ",,a,,b,,");
+ verifyEquals("${input:trimDelimitedList(',')}", attributes, "a,,b");
+
+ attributes.put("input", ",,,a,,b,,c,,,");
+ verifyEquals("${input:trimDelimitedList(',')}", attributes, "a,,b,,c");
+
+ attributes.put("input", ",,,");
+ verifyEquals("${input:trimDelimitedList(',')}", attributes, "");
+
+ attributes.put("input", "a,b,c");
+ verifyEquals("${input:trimDelimitedList(',')}", attributes, "a,b,c");
+
+ attributes.put("input", "a");
+ verifyEquals("${input:trimDelimitedList(',')}", attributes, "a");
+
+ attributes.put("input", "");
+ verifyEquals("${input:trimDelimitedList(',')}", attributes, "");
+
+ attributes.put("input", ",");
+ verifyEquals("${input:trimDelimitedList(',')}", attributes, "");
+
+ // Pipe delimiter — interior empties preserved
+ attributes.put("input", "|a||b|c|");
+ verifyEquals("${input:trimDelimitedList('|')}", attributes, "a||b|c");
+
+ // Multi-character delimiter — interior empties preserved
+ attributes.put("input", "::a::::b::c::");
+ verifyEquals("${input:trimDelimitedList('::')}", attributes,
"a::::b::c");
+
+ // Regex-special character delimiter
+ attributes.put("input", "..a....b..c..");
+ verifyEquals("${input:trimDelimitedList('..')}", attributes,
"a....b..c");
+
+ // Whitespace-only tokens are non-empty
+ attributes.put("input", ", ,a, ,");
+ verifyEquals("${input:trimDelimitedList(',')}", attributes, " ,a, ");
+
+ // Chaining with trim() to also strip whitespace
+ attributes.put("input", " ,,a,,b,, ");
+ verifyEquals("${input:trim():trimDelimitedList(',')}", attributes,
"a,,b");
+
+ // Missing attribute evaluates to empty string
+ verifyEquals("${missing:trimDelimitedList(',')}", attributes, "");
+ }
}
diff --git a/nifi-docs/src/main/asciidoc/expression-language-guide.adoc
b/nifi-docs/src/main/asciidoc/expression-language-guide.adoc
index 63f48e393c5..454b90020dc 100644
--- a/nifi-docs/src/main/asciidoc/expression-language-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/expression-language-guide.adoc
@@ -880,6 +880,74 @@ then the following Expressions will result in the
following values:
+
+[.function]
+=== compactDelimitedList
+
+*Description*: [.description]#Splits the Subject using the specified
delimiter, removes all empty tokens, and rejoins the remaining tokens using the
same delimiter.
+The order of non-empty tokens is preserved. This is useful for normalizing
+delimited attributes that contain leading, trailing, or repeated delimiters.#
+
+*Subject Type*: [.subject]#String#
+
+*Arguments*:
+
+- [.argName]#_delimiter_# : [.argDesc]#The delimiter used to split and rejoin
the Subject.
+This may be a single character or a multi-character string.#
+
+*Return Type*: [.returnType]#String#
+
+*Examples*: If the "my_list" attribute contains the value `,,a,,b,,c,`, the
"tags"
+attribute contains the value `|alpha||beta|||gamma|`, and the "messy" attribute
+contains the value ` ,,a,,b,, `, then the following Expressions will result in
+the following values:
+
+.CompactDelimitedList Examples
+|======================================================================
+| Expression | Value
+| `${my_list:compactDelimitedList(',')}` | `a,b,c`
+| `${tags:compactDelimitedList('\|')}` | `alpha\|beta\|gamma`
+| `${messy:trim():compactDelimitedList(',')}` | `a,b`
+|======================================================================
+
+
+
+
+[.function]
+=== trimDelimitedList
+
+*Description*: [.description]#Splits the Subject using the specified
delimiter, removes leading
+and trailing empty tokens, and rejoins the remaining tokens using
+the same delimiter. Empty tokens between non-empty tokens are preserved. This
+behaves similarly to `trim()`, but operates on delimited token boundaries
rather
+than whitespace characters.#
+
+*Subject Type*: [.subject]#String#
+
+*Arguments*:
+
+- [.argName]#_delimiter_# : [.argDesc]#The delimiter used to split and rejoin
the Subject.
+This may be a single character or a multi-character string.#
+
+*Return Type*: [.returnType]#String#
+
+*Examples*: If the "my_list" attribute contains the value `,,a,,b,,c,,`, the
+"path_parts" attribute contains the value `;;alpha;;beta;;`, and the "messy"
+attribute contains the value ` ,,a,,b,, `, then the following Expressions will
+result in the following values:
+
+.TrimDelimitedList Examples
+|======================================================================
+| Expression | Value
+| `${my_list:trimDelimitedList(',')}` | `a,,b,,c`
+| `${path_parts:trimDelimitedList(';')}` | `alpha;;beta`
+| `${messy:trim():trimDelimitedList(',')}` | `a,,b`
+|======================================================================
+
+
+
+
+
[.function]
=== unique