Skip to content
157 changes: 157 additions & 0 deletions docs/reference/schemas/config/functions/join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
description: Reference for the 'join' DSC configuration document function
ms.date: 08/29/2025
ms.topic: reference
title: join
---

## Synopsis

Joins an array into a single string, separated using a delimiter.

## Syntax

```Syntax
join(inputArray, delimiter)
```

## Description

The `join()` function takes an array and a delimiter.

- Each array element is converted to a string and concatenated with the
delimiter between elements.

The `delimiter` can be any value; it’s converted to a string.

## Examples

### Example 1 - Produce a list of servers

Create a comma-separated string from a list of host names to pass to tools or
APIs that accept CSV input. This example uses [`createArray()`][02] to build
the server list and joins with ", ".

```yaml
# join.example.1.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[join(createArray('web01','web02','web03'), ', ')]"
```

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

```yaml
results:
- name: Echo
type: Microsoft.DSC.Debug/Echo
result:
actualState:
output: web01, web02, web03
messages: []
hadErrors: false
```

### Example 2 - Build a file system path from segments

Join path segments into a single path string. This is useful when composing
paths dynamically from parts.

```yaml
# join.example.2.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[join(createArray('/etc','nginx','sites-enabled'), '/')]"
```

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

```yaml
results:
- name: Echo
type: Microsoft.DSC.Debug/Echo
result:
actualState:
output: /etc/nginx/sites-enabled
messages: []
hadErrors: false
```

### Example 3 - Format a version string from numeric parts

Convert version components (numbers) into a dotted version string. Non-string
elements are converted to strings automatically.

```yaml
# join.example.3.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: "[join(createArray(1,2,3), '.')]"
```

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

```yaml
results:
- name: Echo
type: Microsoft.DSC.Debug/Echo
result:
actualState:
output: 1.2.3
messages: []
hadErrors: false
```

## Parameters

### inputArray

The array whose elements will be concatenated.

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

### delimiter

Any value used between elements. Converted to a string.

```yaml
Type: any
Required: true
Position: 2
```

## Output

Returns a string containing the joined result.

```yaml
Type: string
```

## Related functions

- [`concat()`][00] - Concatenates strings together
- [`string()`][01] - Converts values to strings

<!-- Link reference definitions -->
[00]: ./concat.md
[01]: ./string.md
26 changes: 23 additions & 3 deletions dsc/tests/dsc_functions.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -409,15 +409,35 @@ Describe 'tests for function expressions' {
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
}

It 'join function works for: <expression>' -TestCases @(
@{ expression = "[join(createArray('a','b','c'), '-')]"; expected = 'a-b-c' }
@{ expression = "[join(createArray(), '-')]"; expected = '' }
@{ expression = "[join(createArray(1,2,3), ',')]"; expected = '1,2,3' }
) {
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 'skip function works for: <expression>' -TestCases @(
@{ expression = "[skip(createArray('a','b','c','d'), 2)]"; expected = @('c','d') }
@{ expression = "[skip(createArray('a','b','c','d'), 2)]"; expected = @('c', 'd') }
@{ expression = "[skip('hello', 2)]"; expected = 'llo' }
@{ expression = "[skip(createArray('a','b'), 0)]"; expected = @('a','b') }
@{ expression = "[skip(createArray('a','b'), 0)]"; expected = @('a', 'b') }
@{ expression = "[skip('abc', 0)]"; expected = 'abc' }
@{ expression = "[skip(createArray('a','b'), 5)]"; expected = @() }
@{ expression = "[skip('', 1)]"; expected = '' }
# Negative counts are treated as zero
@{ expression = "[skip(createArray('x','y'), -3)]"; expected = @('x','y') }
@{ expression = "[skip(createArray('x','y'), -3)]"; expected = @('x', 'y') }
@{ expression = "[skip('xy', -1)]"; expected = 'xy' }
) {
param($expression, $expected)
Expand Down
8 changes: 8 additions & 0 deletions dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,14 @@ description = "Returns the index of the first occurrence of an item in an array"
invoked = "indexOf function"
invalidArrayArg = "First argument must be an array"

[functions.join]
description = "Joins the elements of an array into a single string, separated using a delimiter."
invoked = "join function"
invalidArrayArg = "First argument must be an array"
invalidNullElement = "Array elements cannot be null"
invalidArrayElement = "Array elements cannot be arrays"
invalidObjectElement = "Array elements cannot be objects"

[functions.lastIndexOf]
description = "Returns the index of the last occurrence of an item in an array"
invoked = "lastIndexOf function"
Expand Down
128 changes: 128 additions & 0 deletions dsc_lib/src/functions/join.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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 Join {}

fn stringify_value(v: &Value) -> Result<String, DscError> {
match v {
Value::String(s) => Ok(s.clone()),
Value::Number(n) => Ok(n.to_string()),
Value::Bool(b) => Ok(b.to_string()),
Value::Null => Err(DscError::Parser(t!("functions.join.invalidNullElement").to_string())),
Value::Array(_) => Err(DscError::Parser(t!("functions.join.invalidArrayElement").to_string())),
Value::Object(_) => Err(DscError::Parser(t!("functions.join.invalidObjectElement").to_string())),
}
}

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

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

let delimiter = args[1].as_str().unwrap();

if let Some(array) = args[0].as_array() {
let items: Result<Vec<String>, DscError> = array.iter().map(stringify_value).collect();
let items = items?;
return Ok(Value::String(items.join(delimiter)));
}

Err(DscError::Parser(t!("functions.join.invalidArrayArg").to_string()))
}
}

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

#[test]
fn join_array_of_strings() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[join(createArray('a','b','c'), '-')]", &Context::new()).unwrap();
assert_eq!(result, "a-b-c");
}

#[test]
fn join_empty_array_returns_empty() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[join(createArray(), '-')]", &Context::new()).unwrap();
assert_eq!(result, "");
}

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

#[test]
fn join_array_with_null_fails() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[join(createArray('a', null()), ',')]", &Context::new());
assert!(result.is_err());
// The error comes from argument validation, not our function
assert!(result.unwrap_err().to_string().contains("does not accept null arguments"));
}

#[test]
fn join_array_with_array_fails() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[join(createArray('a', createArray('b')), ',')]", &Context::new());
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Arguments must all be arrays") || error_msg.contains("mixed types"));
}

#[test]
fn join_array_with_object_fails() {
let mut parser = Statement::new().unwrap();
let result = parser.parse_and_execute("[join(createArray('a', createObject('key', 'value')), ',')]", &Context::new());
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Arguments must all be") || error_msg.contains("mixed types"));
}

#[test]
fn join_direct_test_with_mixed_array() {
use serde_json::json;
use crate::configure::context::Context;

let join_fn = Join::default();
let args = vec![
json!(["hello", {"key": "value"}]), // Array with string and object
json!(",")
];
let result = join_fn.invoke(&args, &Context::new());

assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Array elements cannot be objects"));
}
}
2 changes: 2 additions & 0 deletions dsc_lib/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub mod less_or_equals;
pub mod format;
pub mod int;
pub mod index_of;
pub mod join;
pub mod last_index_of;
pub mod max;
pub mod min;
Expand Down Expand Up @@ -148,6 +149,7 @@ impl FunctionDispatcher {
Box::new(format::Format{}),
Box::new(int::Int{}),
Box::new(index_of::IndexOf{}),
Box::new(join::Join{}),
Box::new(last_index_of::LastIndexOf{}),
Box::new(max::Max{}),
Box::new(min::Min{}),
Expand Down
Loading