Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions docs/reference/schemas/config/functions/last.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
---
description: Reference for the 'last' DSC configuration document function
ms.date: 01/25/2025
ms.topic: reference
title: last
---

## Synopsis

Returns the last element of an array, or the last character of a string.

## Syntax

```Syntax
last(arg)
```

## Description

The `last()` function returns the final element from an array or the final
character from a string. This is useful when you need to access the most recent
item in a sequence, the final stage in a deployment pipeline, or the last
character in a configuration value.

For arrays, it returns the element at index `length - 1`. For strings, it
returns the last character as a string.

## Examples

### Example 1 - Extract the final deployment stage (array of strings)

Use `last()` to retrieve the final stage in a multi-stage deployment pipeline.
This helps you identify which environment or phase should receive special
handling, such as extended health checks or manual approval gates. This example
uses [`createArray()`][01] to build the deployment stages.

```yaml
# last.example.1.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Deployment Pipeline
type: Microsoft.DSC.Debug/Echo
properties:
output:
finalStage: "[last(createArray('dev', 'test', 'staging', 'production'))]"
requiresApproval: true
```

```bash
dsc config get --file last.example.1.dsc.config.yaml
```

```yaml
results:
- name: Deployment Pipeline
type: Microsoft.DSC.Debug/Echo
result:
actualState:
output:
finalStage: production
requiresApproval: true
messages: []
hadErrors: false
```

This identifies `production` as the final stage, allowing you to apply
production-specific policies or validations.

### Example 2 - Get the last character of a configuration string

Use `last()` to extract the final character from a string value. This is useful
for parsing identifiers, checking suffixes, or validating format conventions
like version numbers or region codes.

```yaml
# last.example.2.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Region Identifier
type: Microsoft.DSC.Debug/Echo
properties:
output:
regionCode: us-west-2
zoneSuffix: "[last('us-west-2')]"
description: "Zone suffix extracted from region code"
```

```bash
dsc config get --file last.example.2.dsc.config.yaml
```

```yaml
results:
- name: Region Identifier
type: Microsoft.DSC.Debug/Echo
result:
actualState:
output:
regionCode: us-west-2
zoneSuffix: '2'
description: Zone suffix extracted from region code
messages: []
hadErrors: false
```

The function returns `'2'` as a single-character string, representing the zone
suffix in the region identifier.

### Example 3 - Identify the most recent backup (array of numbers)

Use `last()` with numerical arrays to find the most recent timestamp or version
number. This example shows how to select the latest backup from a sorted list
of timestamps. This example uses [`createArray()`][01] to build the backup
timestamps.

```yaml
# last.example.3.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Backup Selection
type: Microsoft.DSC.Debug/Echo
properties:
output:
availableBackups: "[createArray(1704067200, 1704153600, 1704240000, 1704326400)]"
latestBackup: "[last(createArray(1704067200, 1704153600, 1704240000, 1704326400))]"
description: "Most recent backup timestamp (Unix epoch)"
```

```bash
dsc config get --file last.example.3.dsc.config.yaml
```

```yaml
results:
- name: Backup Selection
type: Microsoft.DSC.Debug/Echo
result:
actualState:
output:
availableBackups:
- 1704067200
- 1704153600
- 1704240000
- 1704326400
latestBackup: 1704326400
description: Most recent backup timestamp (Unix epoch)
messages: []
hadErrors: false
```

The function returns `1704326400`, which represents the most recent backup in
the chronologically sorted array.

### Example 4 - Combine with other functions for complex logic

Use `last()` together with [`split()`][02] to extract file extensions or path
components. This example demonstrates parsing a filename to get its extension.

```yaml
# last.example.4.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: File Extension Parser
type: Microsoft.DSC.Debug/Echo
properties:
output:
filename: config.production.yaml
extension: "[last(split('config.production.yaml', '.'))]"
```

```bash
dsc config get --file last.example.4.dsc.config.yaml
```

```yaml
results:
- name: File Extension Parser
type: Microsoft.DSC.Debug/Echo
result:
actualState:
output:
filename: config.production.yaml
extension: yaml
messages: []
hadErrors: false
```

By combining `split()` and `last()`, you can extract the `yaml` extension from
the full filename.

## Parameters

### arg

The array or string to get the last element or character from. Required.

```yaml
Type: array | string
Required: true
Position: 1
```

## Output

Returns the last element of the array (preserving its original type) or the
last character as a string. For arrays, the return type matches the element
type. For strings, returns a single-character string.

If the input is an empty array, the function returns `null`. If the input is an
empty string, the function returns an empty string.

```yaml
Type: any | string | null
```

## Errors

The function returns an error in the following cases:

- **Invalid type**: The argument is not an array or string

## Related functions

- [`first()`][00] - Returns the first element of an array or character of a string
- [`split()`][02] - Splits a string into an array
- [`createArray()`][01] - Creates an array from provided values

<!-- Link reference definitions -->
[00]: ./first.md
[01]: ./createArray.md
[02]: ./split.md
24 changes: 24 additions & 0 deletions dsc/tests/dsc_functions.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,30 @@ Describe 'tests for function expressions' {
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
}

It 'last function works for: <expression>' -TestCases @(
@{ expression = "[last(createArray('hello', 'world'))]"; expected = 'world' }
@{ expression = "[last(createArray(1, 2, 3))]"; expected = 3 }
@{ expression = "[last('hello')]"; expected = 'o' }
@{ expression = "[last('a')]"; expected = 'a' }
@{ expression = "[last(array('mixed'))]"; expected = 'mixed' }
@{ expression = "[last(createArray())]"; expected = $null }
@{ expression = "[last('')]"; expected = '' }
) {
param($expression, $expected)

$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "$expression"
"@
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
}

It 'indexOf function works for: <expression>' -TestCases @(
@{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'banana')]"; expected = 1 }
@{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'cherry')]"; expected = 2 }
Expand Down
5 changes: 5 additions & 0 deletions lib/dsc-lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,11 @@ emptyArray = "Cannot get first element of empty array"
emptyString = "Cannot get first character of empty string"
invalidArgType = "Invalid argument type, argument must be an array or string"

[functions.last]
description = "Returns the last element of an array or last character of a string"
invoked = "last function"
invalidArgType = "Invalid argument type, argument must be an array or string"

[functions.greater]
description = "Evaluates if the first value is greater than the second value"
invoked = "greater function"
Expand Down
116 changes: 116 additions & 0 deletions lib/dsc-lib/src/functions/last.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::DscError;
use crate::configure::context::Context;
use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata};
use rust_i18n::t;
use serde_json::Value;
use tracing::debug;

#[derive(Debug, Default)]
pub struct Last {}

impl Function for Last {
fn get_metadata(&self) -> FunctionMetadata {
FunctionMetadata {
name: "last".to_string(),
description: t!("functions.last.description").to_string(),
category: vec![FunctionCategory::Array, FunctionCategory::String],
min_args: 1,
max_args: 1,
accepted_arg_ordered_types: vec![vec![FunctionArgKind::Array, FunctionArgKind::String]],
remaining_arg_accepted_types: None,
return_types: vec![FunctionArgKind::String, FunctionArgKind::Number, FunctionArgKind::Array, FunctionArgKind::Object, FunctionArgKind::Null],
}
}

fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
debug!("{}", t!("functions.last.invoked"));

if let Some(array) = args[0].as_array() {
if array.is_empty() {
return Ok(Value::Null);
}
return Ok(array[array.len() - 1].clone());
}

if let Some(string) = args[0].as_str() {
if string.is_empty() {
return Ok(Value::String(String::new()));
}
return Ok(Value::String(string.chars().last().unwrap().to_string()));
}

Err(DscError::Parser(t!("functions.last.invalidArgType").to_string()))
}
}

#[cfg(test)]
mod tests {
use crate::configure::context::Context;
use crate::parser::Statement;

#[test]
fn array_of_strings() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[last(createArray('hello', 'world'))]", &Context::new()).unwrap();
assert_eq!(result.as_str(), Some("world"));
}

#[test]
fn array_of_numbers() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[last(createArray(1, 2, 3))]", &Context::new()).unwrap();
assert_eq!(result.to_string(), "3");
}

#[test]
fn array_of_single_element() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[last(array('hello'))]", &Context::new()).unwrap();
assert_eq!(result.as_str(), Some("hello"));
}

#[test]
fn string_input() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[last('hello')]", &Context::new()).unwrap();
assert_eq!(result.as_str(), Some("o"));
}

#[test]
fn single_character_string() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[last('a')]", &Context::new()).unwrap();
assert_eq!(result.as_str(), Some("a"));
}

#[test]
fn invalid_type_object() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[last(createObject('key', 'value'))]", &Context::new());
assert!(result.is_err());
}

#[test]
fn invalid_type_number() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[last(42)]", &Context::new());
assert!(result.is_err());
}

#[test]
fn array_of_multiple_strings() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[last(createArray('text', 'middle', 'last'))]", &Context::new()).unwrap();
assert_eq!(result.as_str(), Some("last"));
}

#[test]
fn unicode_string() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[last('Hello🌍')]", &Context::new()).unwrap();
assert_eq!(result.as_str(), Some("🌍"));
}
}
Loading