Skip to content

Conversation

@SteveL-MSFT
Copy link
Member

@SteveL-MSFT SteveL-MSFT commented Oct 21, 2025

PR Summary

Adds tryWhich() function which either returns the full path to an executable or null if not found.
This function is useful with a new condition property in resource and extension manifests such that if the condition evaluates to false then that manifest is skipped (where currently you might see a warning message because the exe specified isn't on the system). Manifests can use any function in the expression that does not require deployment (like reference(), for example).

This change also updates the bicep extension to use this to check if bicep is on the system.

Finally, the equals() function was updated to allow checking against null since it appears that the ARM implementation allows for this even though the docs don't specify it.

Example resource manifest that detects if running Windows:

{
    "`$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
    "type": "Test/MyEcho",
    "version": "1.0.0",
    "condition": "[equals(context().os.family,'Windows')]",
    "get": {
        "executable": "dscecho",
        "args": [
            {
                "jsonInputArg": "--input",
                "mandatory": true
            }
        ]
    },
    "schema": {
        "command": {
            "executable": "dscecho"
        }
    }
}

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a tryWhich() function to locate executables in the system PATH and adds conditional manifest loading functionality to skip resources or extensions when conditions aren't met.

Key Changes:

  • Adds tryWhich() function that returns the full path to an executable or null if not found
  • Enables resource and extension manifests to include a condition property for conditional loading
  • Updates the equals() function to support null comparisons

Reviewed Changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
lib/dsc-lib/src/functions/try_which.rs Implements the new tryWhich() function with path resolution logic
lib/dsc-lib/src/functions/mod.rs Registers the tryWhich function in the dispatcher
lib/dsc-lib/src/functions/equals.rs Adds null support to the equals() function arguments
lib/dsc-lib/src/extensions/extension_manifest.rs Adds optional condition field to extension manifests
lib/dsc-lib/src/extensions/discover.rs Updates extension discovery to handle conditional manifests
lib/dsc-lib/src/dscresources/resource_manifest.rs Adds optional condition field to resource manifests
lib/dsc-lib/src/discovery/command_discovery.rs Implements condition evaluation logic for manifest loading
lib/dsc-lib/locales/en-us.toml Adds localized strings for condition handling and tryWhich function
extensions/bicep/bicep.dsc.extension.json Uses the new condition feature to check for bicep executable availability
dsc/tests/dsc_resource_manifest.tests.ps1 Tests resource manifest condition evaluation
dsc/tests/dsc_functions.tests.ps1 Tests the tryWhich() function behavior
dsc/tests/dsc_extension_manifest.tests.ps1 Tests extension manifest condition evaluation

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@Gijsreyn
Copy link
Contributor

Gijsreyn commented Oct 21, 2025

@SteveL-MSFT - can you perhaps update the following doc? Also add an example like:

### Example 4 - Compare incompatible types

The following example shows that `equals()` returns `null` when comparing incompatible types,
such as comparing a string with an integer or an array with an object.

```yaml
# equals.example.4.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Compare incompatible types
  type: Microsoft.DSC.Debug/Echo
  properties:
    output:
      stringAndInteger: "[equals('1', 1)]"
      arrayAndObject: "[equals(createArray('a', 'b'), createObject('key', 'value'))]"
      stringAndArray: "[equals('hello', createArray('h', 'e', 'l', 'l', 'o'))]"
```

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

```yaml
results:
- name: Compare incompatible types
  type: Microsoft.DSC.Debug/Echo
  result:
    actualState:
      output:
        stringAndInteger: null
        arrayAndObject: null
        stringAndArray: null
messages: []
hadErrors: false
```

In the output, I guess you can do:

## Output

The `equals()` function returns `true` if the input values are the same, `false` if they are
different but of compatible types, and `null` if the values are of incompatible types that
cannot be compared.

```yaml
Type: bool | null
```

Can you also add a tryWhich.md, something like:

---
description: Reference for the 'tryWhich' DSC configuration document function
ms.date:     01/17/2025
ms.topic:    reference
title:       tryWhich
---

# tryWhich

## Synopsis

Returns the full path to an executable if it exists in the system PATH, otherwise returns null.

## Syntax

```Syntax
tryWhich(<executableName>)
```

## Description

The `tryWhich()` function searches for an executable in the system PATH and returns its full
path if found. If the executable doesn't exist in the PATH, the function returns `null` instead
of raising an error. This makes it useful for conditional logic where you need to check if a
command-line tool is available before using it.

This function is particularly useful with the `condition` property in resource and extension
manifests. When the condition evaluates to false, DSC skips that manifest, avoiding warning
messages that would otherwise appear when the specified executable isn't on the system.

## Examples

### Example 1 - Check if a command exists

The following example checks whether the `git` command is available on the system.

```yaml
# tryWhich.example.1.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Check for git
  type: Microsoft.DSC.Debug/Echo
  properties:
    output:
      gitPath: "[tryWhich('git')]"
      hasGit: "[not(equals(tryWhich('git'), null()))]"
```

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

```yaml
results:
- name: Check for git
  type: Microsoft.DSC.Debug/Echo
  result:
    actualState:
      output:
        gitPath: /usr/bin/git
        hasGit: true
messages: []
hadErrors: false
```

### Example 2 - Conditional resource based on tool availability

The following example demonstrates using `tryWhich()` with the `if()` function to conditionally
set a property based on whether a tool is installed.

```yaml
# tryWhich.example.2.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Tool availability status
  type: Microsoft.DSC.Debug/Echo
  properties:
    output:
      message: "[if(not(equals(tryWhich('docker'), null())), 'Docker is available', 'Docker is not installed')]"
```

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

```yaml
results:
- name: Tool availability status
  type: Microsoft.DSC.Debug/Echo
  result:
    actualState:
      output:
        message: Docker is available
messages: []
hadErrors: false
```

### Example 3 - Compare with environment-specific executables

The following example checks for different package managers across operating systems.

```yaml
# tryWhich.example.3.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Package manager detection
  type: Microsoft.DSC.Debug/Echo
  properties:
    output:
      hasApt: "[not(equals(tryWhich('apt'), null()))]"
      hasBrew: "[not(equals(tryWhich('brew'), null()))]"
      hasChoco: "[not(equals(tryWhich('choco'), null()))]"
```

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

```yaml
results:
- name: Package manager detection
  type: Microsoft.DSC.Debug/Echo
  result:
    actualState:
      output:
        hasApt: true
        hasBrew: false
        hasChoco: false
messages: []
hadErrors: false
```

### Example 4 - Build dynamic paths with available tools

The following example uses `tryWhich()` with [`coalesce()`][01] to select the first available
tool from a list.

```yaml
# tryWhich.example.4.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Find available editor
  type: Microsoft.DSC.Debug/Echo
  properties:
    output:
      editor: "[coalesce(tryWhich('code'), tryWhich('vim'), tryWhich('nano'), 'notepad')]"
```

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

```yaml
results:
- name: Find available editor
  type: Microsoft.DSC.Debug/Echo
  result:
    actualState:
      output:
        editor: /usr/local/bin/code
messages: []
hadErrors: false
```

### Example 5 - Use with manifest conditions

The following example shows how `tryWhich()` can be used in a manifest's `condition` property
to skip resources when required tools aren't available. This prevents warning messages about
missing executables.

```yaml
# tryWhich.example.5.dsc.config.yaml
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Docker container status
  type: Docker/Container
  condition: "[not(equals(tryWhich('docker'), null()))]"
  properties:
    name: my-app
    image: nginx:latest
```

In this example, the Docker/Container resource is only processed if the `docker` command is
available in the system PATH. If docker is not installed, the condition evaluates to `false`
and DSC skips the resource without showing warning messages about the missing executable.

## Parameters

### executableName

The name of the executable to search for in the system PATH. This should be the command name
as you would type it in a terminal (e.g., `git`, `docker`, `python`). On Windows, you can
specify the name with or without the `.exe` extension.

```yaml
Type:         string
Required:     true
MinimumCount: 1
MaximumCount: 1
```

## Output

The `tryWhich()` function returns the full path to the executable as a string if it exists in
the system PATH. If the executable is not found, the function returns `null`.

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

## Related functions

- [`envvar()`][02] - Retrieves environment variable values
- [`if()`][03] - Conditional logic
- [`equals()`][04] - Compares two values for equality
- [`null()`][05] - Returns a null value
- [`coalesce()`][01] - Returns the first non-null value
- [`not()`][06] - Logical negation

<!-- Link reference definitions -->
[01]: ./coalesce.md
[02]: ./envvar.md
[03]: ./if.md
[04]: ./equals.md
[05]: ./null.md
[06]: ./not.md

If you like to separate the doc updates, let me know, then I'll update it after it gets merged.

Note

I couldn't fully test out the examples, as I was a bit lazy to build your branch.

@SteveL-MSFT
Copy link
Member Author

SteveL-MSFT commented Oct 21, 2025

@Gijsreyn I think we can do the doc updates separately. Thanks for your help though!

@SteveL-MSFT
Copy link
Member Author

@Gijsreyn I'll just add that you do a great job with the doc content, but I would say that's not one of my strengths :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants