diff --git a/docs/CHANGELOG-v2.md b/docs/CHANGELOG-v2.md
index 5ac2ad1a52..4f83e19a7f 100644
--- a/docs/CHANGELOG-v2.md
+++ b/docs/CHANGELOG-v2.md
@@ -35,6 +35,9 @@ What's changed since pre-release v2.7.0-B0016:
- New features:
- Added API version date comparison assertion method and expression by @BernieWhite.
[#1356](https://github.com/microsoft/PSRule/issues/1356)
+ - Added support for new functions by @BernieWhite.
+ [#1227](https://github.com/microsoft/PSRule/issues/1227)
+ - Added support for `trim`, `replace`, `split`, `first`, and `last`.
- Bug fixes:
- Fixes CLI failed to load required assemblies by @BernieWhite.
[#1361](https://github.com/microsoft/PSRule/issues/1361)
diff --git a/docs/expressions/functions.md b/docs/expressions/functions.md
index 312dc77a06..7269c74910 100644
--- a/docs/expressions/functions.md
+++ b/docs/expressions/functions.md
@@ -22,12 +22,17 @@ Functions cover two (2) main scenarios:
It may be necessary to perform minor transformation before evaluating a condition.
- `boolean` - Convert a value to a boolean.
-- `string` - Convert a value to a string.
-- `integer` - Convert a value to an integer.
- `concat` - Concatenate multiple values.
-- `substring` - Extract a substring from a string.
- `configuration` - Get a configuration value.
+- `first` - Return the first element in an array or the first character of a string.
+- `integer` - Convert a value to an integer.
+- `last` - Return the last element in an array or the last character of a string.
- `path` - Get a value from an object path.
+- `replace` - Replace an old string with a new string.
+- `split` - Split a string into an array by a delimiter.
+- `string` - Convert a value to a string.
+- `substring` - Extract a substring from a string.
+- `trim` - Remove whitespace from the start and end of a string.
## Supported conditions
diff --git a/schemas/PSRule-language.schema.json b/schemas/PSRule-language.schema.json
index b30580c5c6..d34c8c3597 100644
--- a/schemas/PSRule-language.schema.json
+++ b/schemas/PSRule-language.schema.json
@@ -3034,6 +3034,21 @@
},
{
"$ref": "#/definitions/fn/definitions/function/definitions/configuration"
+ },
+ {
+ "$ref": "#/definitions/fn/definitions/function/definitions/replace"
+ },
+ {
+ "$ref": "#/definitions/fn/definitions/function/definitions/trim"
+ },
+ {
+ "$ref": "#/definitions/fn/definitions/function/definitions/first"
+ },
+ {
+ "$ref": "#/definitions/fn/definitions/function/definitions/last"
+ },
+ {
+ "$ref": "#/definitions/fn/definitions/function/definitions/split"
}
],
"definitions": {
@@ -3305,6 +3320,159 @@
"required": [
"configuration"
]
+ },
+ "replace": {
+ "type": "object",
+ "properties": {
+ "replace": {
+ "type": "object",
+ "title": "Trim",
+ "description": "The replace function searches for a string and replaces them with a new string.",
+ "markdownDescription": "The `replace` function searches for a string and replaces them with a new string.",
+ "$ref": "#/definitions/fn/definitions/function"
+ },
+ "oldString": {
+ "type": "string",
+ "description": "The string to replace.",
+ "minLength": 1
+ },
+ "newString": {
+ "type": "string",
+ "description": "The new string to replace oldString."
+ },
+ "caseSensitive": {
+ "type": "boolean",
+ "title": "Case-sensitive",
+ "description": "Determines if the replace is case-sensitive. By default, a case-insensitive comparison is performed.",
+ "default": false
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "replace",
+ "oldString",
+ "newString"
+ ]
+ },
+ "trim": {
+ "type": "object",
+ "properties": {
+ "trim": {
+ "type": "object",
+ "title": "Trim",
+ "description": "Trim a string value by removing leading and trailings whitespace.",
+ "markdownDescription": "Trim a string value by removing leading and trailings whitespace.",
+ "$ref": "#/definitions/fn/definitions/function"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "trim"
+ ]
+ },
+ "first": {
+ "type": "object",
+ "properties": {
+ "first": {
+ "oneOf": [
+ {
+ "type": "array",
+ "description": "The first function returns the first element in the array.",
+ "markdownDescription": "The `first` function returns the first element in the array.",
+ "items": {
+ "$ref": "#/definitions/fn/definitions/function"
+ },
+ "additionalItems": false,
+ "minItems": 2
+ },
+ {
+ "type": "object",
+ "description": "The first function returns the first element of arrays or the first character of a string.",
+ "markdownDescription": "The `first` function returns the first element of arrays or the first character of a string.",
+ "$ref": "#/definitions/fn/definitions/function"
+ }
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "first"
+ ]
+ },
+ "last": {
+ "type": "object",
+ "properties": {
+ "last": {
+ "oneOf": [
+ {
+ "type": "array",
+ "description": "The last function returns the last element in the array.",
+ "markdownDescription": "The `last` function returns the last element in the array.",
+ "items": {
+ "$ref": "#/definitions/fn/definitions/function"
+ },
+ "additionalItems": false,
+ "minItems": 2
+ },
+ {
+ "type": "object",
+ "description": "The last function returns the last element of arrays or the last character of a string.",
+ "markdownDescription": "The `last` function returns the last element of arrays or the last character of a string.",
+ "$ref": "#/definitions/fn/definitions/function"
+ }
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "last"
+ ]
+ },
+ "split": {
+ "type": "object",
+ "properties": {
+ "split": {
+ "oneOf": [
+ {
+ "type": "string",
+ "description": "The split function returns converts a string into an array by spliting based on a delimiter.",
+ "markdownDescription": "The `split` function returns converts a string into an array by spliting based on a delimiter."
+ },
+ {
+ "type": "object",
+ "description": "The split function returns converts a string into an array by spliting based on a delimiter.",
+ "markdownDescription": "The `split` function returns converts a string into an array by spliting based on a delimiter.",
+ "$ref": "#/definitions/fn/definitions/function"
+ }
+ ],
+ "default": {}
+ },
+ "delimiter": {
+ "oneOf": [
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ },
+ "minItems": 1,
+ "additionalItems": false
+ }
+ ],
+ "default": [
+ ""
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "split",
+ "delimiter"
+ ]
}
}
}
diff --git a/src/PSRule/Common/ArrayExtensions.cs b/src/PSRule/Common/ArrayExtensions.cs
new file mode 100644
index 0000000000..2caf42f5eb
--- /dev/null
+++ b/src/PSRule/Common/ArrayExtensions.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace PSRule.Common
+{
+ ///
+ /// Extension methods for arrays.
+ ///
+ internal static class ArrayExtensions
+ {
+ internal static object Last(this Array array)
+ {
+ return array.Length > 0 ? array.GetValue(array.Length - 1) : null;
+ }
+
+ internal static object First(this Array array)
+ {
+ return array.Length > 0 ? array.GetValue(0) : null;
+ }
+ }
+}
diff --git a/src/PSRule/Common/ExpressionHelpers.cs b/src/PSRule/Common/ExpressionHelpers.cs
index 3f6e51f8c4..8cd3e47a69 100644
--- a/src/PSRule/Common/ExpressionHelpers.cs
+++ b/src/PSRule/Common/ExpressionHelpers.cs
@@ -7,6 +7,7 @@
using System.IO;
using System.Linq;
using System.Management.Automation;
+using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using Newtonsoft.Json.Linq;
@@ -57,6 +58,9 @@ internal static bool Equal(object expectedValue, object actualValue, bool caseSe
if (TryInt(expectedValue, convertExpected, out var i1) && TryInt(actualValue, convertActual, out var i2))
return i1 == i2;
+ if (TryArray(expectedValue, out var a1) && TryArray(actualValue, out var a2))
+ return SequenceEqual(a1, a2);
+
var expectedBase = GetBaseObject(expectedValue);
var actualBase = GetBaseObject(actualValue);
if (expectedBase == null || actualBase == null)
@@ -65,6 +69,69 @@ internal static bool Equal(object expectedValue, object actualValue, bool caseSe
return expectedBase.Equals(actualBase) || expectedValue.Equals(actualValue);
}
+ internal static bool SequenceEqual(Array array1, Array array2)
+ {
+ if (array1.Length != array2.Length)
+ return false;
+
+ for (var i = 0; i < array1.Length; i++)
+ {
+ if (!Equal(array1.GetValue(i), array2.GetValue(i)))
+ return false;
+ }
+ return true;
+ }
+
+ internal static bool Equal(object o1, object o2)
+ {
+ // One null
+ if (o1 == null || o2 == null)
+ return o1 == o2;
+
+ // Arrays
+ if (o1 is Array array1 && o2 is Array array2)
+ return SequenceEqual(array1, array2);
+ else if (o1 is Array || o2 is Array)
+ return false;
+
+ // String and int
+ if (TryString(o1, out var s1) && TryString(o2, out var s2))
+ return s1 == s2;
+ else if (TryString(o1, out _) || TryString(o2, out _))
+ return false;
+ else if (TryLong(o1, false, out var i1) && TryLong(o2, false, out var i2))
+ return i1 == i2;
+ else if (TryLong(o1, false, out var _) || TryLong(o2, false, out var _))
+ return false;
+
+ // JTokens
+ if (o1 is JToken t1 && o2 is JToken t2)
+ return JTokenEquals(t1, t2);
+
+ // Objects
+ return ObjectEquals(o1, o2);
+ }
+
+ private static bool JTokenEquals(JToken t1, JToken t2)
+ {
+ return JToken.DeepEquals(t1, t2);
+ }
+
+ internal static bool ObjectEquals(object o1, object o2)
+ {
+ var objectType = o1.GetType();
+ if (objectType != o2.GetType())
+ return false;
+
+ var props = objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty);
+ for (var i = 0; i < props.Length; i++)
+ {
+ if (!object.Equals(props[i].GetValue(o1), props[i].GetValue(o2)))
+ return false;
+ }
+ return true;
+ }
+
internal static int Compare(object left, object right)
{
if (TryString(left, out var stringLeft) && TryString(right, out var stringRight))
@@ -162,13 +229,18 @@ internal static bool TryString(object o, bool convert, out string value)
internal static bool TryArray(object o, out Array value)
{
o = GetBaseObject(o);
+ value = null;
+ if (o is string) return false;
if (o is Array a)
- {
value = a;
- return true;
- }
- value = null;
- return false;
+
+ else if (o is JArray jArray)
+ value = jArray.Values