Welcome to the third post in this series about “Scripting like a pro”, great to have you here! 😊
In this post, we’ll take your scripting skills to the next level by building on everything you’ve learned so far and pushing it even further.
By now, you should be familiar with:

  • Objects
  • Functions
  • Classes
  • Strongly typed variables
  • Data structuring and storing of data

(If you missed any of these topics, be sure to check out my previous posts! 😉)
Now, it’s time to move forward and explore the next key concepts on our journey:

  • Leveling up to advanced functions
  • All sorts of attributes
  • Impact handlers
  • Parameter guidance
  • Validation to the next level
    Get ready to elevate your scripting expertise to new heights! 🚀

Leveling up to advanced functions

As mentioned in the previous posts, functions often return values that can be used for further processing. In this post, we will continue building on the user management system from previous posts and enhance it even further.

class User {
    [string]$userName

    [int]$age

    User([string]$UserName, [int]$Age) {
        $this.userName = $UserName
        $this.age = $Age
    }
}

class UserManagement {
    [System.Collections.Generic.List[User]]$users = @()

    UserManagement() {
        $this.users.Add((New-Object User -ArgumentList "John", 25))
        $this.users.Add((New-Object User -ArgumentList "Jane", 30))
    }
}

[UserManagement]$userManagement = New-Object UserManagement

function Set-User {
    [CmdletBinding()]
    param (       
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [User]$User,

        [Parameter()]        
        [int]$Age
    )

    begin {
        write-verbose "Setting up the Set-User function"
    }

    process {      
        $User = $User | Set-UserAge -Age $Age     
    }

    end {
        return $User
    }
}

Let’s break down what this code does:

  • [CmdletBinding()] – This is an attribute that identifies the function as a cmdlet, enabling advanced features such as parameter binding
  • Param() – Defines the parameters required for the function to run
  • Begin {} – Executes before the function processes any input. This is commonly used for initializing variables, setting up resources, or performing pre-execution checks—think of it as a “pre-flight” check
  • Process {} – Contains the core business logic of the function. It runs once per input item when the function is called
  • End {} – Executes after all input has been processed. Typically used for resource cleanup, closing connections, or summarizing results. Now let’s implement some more advanced functions
function Set-UserAge {
    [CmdletBinding()]
    param (       
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [User]$User,

        [Parameter()]        
        [int]$Age
    )

    begin {
        write-verbose "Setting up the Set-UserAge function"
    }

    process {
        if(0 -ne $age) {
            $User.age = $Age
        }
        $User.age = $Age       
    }

    end {
        return $User
    }
}

Explanation:

  • ValueFromPipeline – Allows the function to accept input directly from the output of a previous function, enabling streamlined processing see this as ‘take from the left, give to the right’ (same with piping)
  • -ne (Not Equal) – Checks if the left-hand value does not match the right-hand value.

What This Code Does:

If the age parameter is not 0, the function updates the user’s current age with the provided value. This function is now self-contained, meaning the logic for setting an age is handled entirely within it.

Once the function completes execution, the updated [User] object is returned to whatever called it. In our case, when Set-User calls Set-UserAge, we delegate the age-setting logic to Set-UserAge, keeping our code clean and readable. This modular approach ensures better maintainability. If we were to modify the logic, we could do so without affecting other parts of the code

process {
        if(0 -ne $age -and $age -ge $User.age  ) {
            $User.age = $Age
        }        
    }

We have now ensured that the user’s age can only be updated if the provided age parameter is equal to or greater than the user’s current age. Now, when we execute the following:

$userManagement.users | ForEach-Object {
    $fetchedUser = $_ | Set-User -Age 25 -verbose 
    $fetchedUser;
}

💡 See Write-Verbose? This shows output which normally would be hidden which might be considered as ‘less relevant’

Logic Explanation:

For each user object within $userManagement.Users, we define a variable $fetchedUsers. This variable will store the value of the current iteration after Set-User returns the updated user object with the age set to 25, as specified in the parameter.

Hope this helps clarify things! 😉

All sorts of attributes

By implementing [CmdletBinding()], our function gains several additional functionalities! there are many other attributes, each with unique properties, that can further enhance the quality of our code.

Now, let’s extend our parameter block by adding an extra parameter.

[Parameter()]
[switch]$ConvertToJSON

This is a switch type. By default, it is set to false unless explicitly specified. PowerShell already includes many built-in switches like -AsJob. These switches are essentially flags that are evaluated in the code based on whether they are set or not.

In this example, the code would look like the following:

  end {
        if($ConvertToJSON) {
            return $User | ConvertTo-Json
        }
        return $User
    }

What we’re doing here is saying: if the ConvertToJSON flag is set, the function will return the User object after converting it to JSON, rather than returning the object in its original form. By using flags like this, you can add extra functionality to your code with minimal impact. If you call your function from multiple places, the already existing calling function(s) won’t be affected unless they explicitly specify the flag. 😉

Dynamic parameters

Imagine you need to provide parameters for your functions, but only under specific conditions. You can achieve this by implementing something called DynamicParam. Below is an example of how to implement this, along with a demonstration of when it serves its purpose.

dynamicparam {
        # Create the dictionary to hold dynamic parameters
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()

        # Since $ConvertToJSON isn't available in dynamicparam, check if it's passed via the command line
        if ($MyInvocation.BoundParameters.ContainsKey('ConvertToJSON')) {
            # Define the parameter attributes
            $paramAttribute = [System.Management.Automation.ParameterAttribute]@{
                Mandatory = $false
            }

            # Create a collection of attributes for the dynamic parameter
            $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
            $attributeCollection.Add($paramAttribute)

            # Create the dynamic parameter (name, type, attributes)
            $dynParam = [System.Management.Automation.RuntimeDefinedParameter]::new(
                'ManipulateAge', [string], $attributeCollection
            )

            # Store it in the dictionary
            $paramDictionary.Add('ManipulateAge', $dynParam)
        }
        return $paramDictionary
    }

This code checks if the parameter ConvertToJSON is set. If that’s the case, a new parameter called ManipulateAge is added dynamically. This ensures that ManipulateAge can only be provided when ConvertToJSON is supplied. In this way, we enforce the constraint that ConvertToJSON must be set before the dynamic ManipulateAge parameter can be used.

You can observe the output in the console as follows:

If you attempt to set the ManipulateAge parameter without first specifying the ConvertToJSON variable, you will see that the operation fails. This ensures that the constraint is properly enforced.

That’s because the parameter is only available when the specified conditions are met. 😉

Some good use cases for this approach include:

  • Chained parameter availability
  • Environment-specific variables (e.g., Windows/Linux?)
  • Dynamic lists or values (e.g., populating a list of users based on a condition)
  • Improved user experience

Impact handlers

Sometimes, the code you want to execute can have a significant impact. In such cases, you may want to include an additional validation step to ensure that the user gives their “final consent” before the function runs. This helps prevent unwanted execution.

For example, we can flag our CmdletBinding like this:

[CmdletBinding(ConfirmImpact='High')]

Don’t forget to modify the code accordingly 😉

 process {         
        if($PSCmdlet.ShouldProcess($User.userName, "Setting Age to $Age")) {
            $User = $User | Set-UserAge -Age $Age
        } else {
            Write-Host "Skipping the user: $($User.userName)"
        }
    }

If you now run the code with;

$userManagement.users | ForEach-Object {

    $fetchedUser = $_ | Set-User -Age 25 -verbose -WhatIf

    $fetchedUser;

}

You will see what the code is going to change!

And if we leave the -WhatIf from the function calling we are getting prompted to confirm our changes;

Talking about reducing risk right? 😉

Parameter guidance

In previous code examples, we’ve seen how to set a variable as mandatory. However, there are additional options available that can make our code more robust and stable by utilizing built-in flags for parameters.

As I’ve emphasized in earlier posts, maintaining clean code is crucial. In this post, I’ll show you how we can make our function even more robust!

  [Parameter(Mandatory=$true, Position = 0, HelpMessage = "The input [User] object", ValueFromPipeline=$true)]
        [User]$User,

In this case, we’ve introduced Position and HelpMessage flags to the parameter for the user object. This enhances the user experience by:

  • Enforcing that the variable is located at position 0 (yes, we start at 0, and from there, we increment 😉).
  • If no user object is specified, the code presents a help message, guiding the user on what is expected at position 0.

💡 Please note that the HelpMessage is only displayed in graphical environments. If you’re working in a shell environment, you can implement a statement wrapper around the variable and return an informative message directly to the shell.

Alias

An alias is a useful tool that makes life easier for script users. It allows you to map multiple nouns or abbreviations to the same variable, providing flexibility in how the script is used.

Here’s how you can configure an alias:

  [Parameter()] 
        [ValidateSet(25, 30, 35)]
        [Alias("Aged", "Aging")]
        [int]$Age,

Now, the Age parameter is not only identified by the name “Age,” but also by aliases like “Aged” and “Aging.” This approach provides flexibility for users, allowing them to use the alias that best fits their intention, while still mapping it to the actual variable needed for processing the script.

However, be cautious make sure that the code remains readable! Overusing aliases can lead to confusion and make the code harder to maintain. It’s easy to create a mess when you’re not careful. 😉

Validating

While parameter validation can certainly be done through statements, there’s an alternative way to simplify this process without repeatedly writing those statements.

In PowerShell, you can use the [ValidateSet()] attribute to enforce a specific set of values for the variables you’re providing. This makes your code more concise and ensures that only valid values are accepted.

[Parameter()] 
        [ValidateSet(25, 30, 35)]       
        [int]$Age,

With this, we’ve enforced that the $age variable can now only contain the values specified in the set (25, 30, 35). If a value outside of this set is provided, the script will notify the user accordingly.

This way, we don’t have to write those checks ourselves—thanks to the power of [ValidateSet()], we can rely on it to handle validation for us. 😉

Pattern validation

Okay, I’ll be honest and I’m sure many of you can agree RegEx (Regular Expressions) is not one of my favorite topics. 🤣

However, it’s an incredibly powerful tool for validating input. You can quickly check if the data matches what you expect, all without having to write countless statements or checks yourself. With just a regular expression, you can handle all of it.

There are many excellent tools available that can help you build these expressions, as they can quickly become difficult to read.

For now, I’ll just cover the basics 😊. If you’re interested in a dedicated post on regular expressions, drop a comment below, and I’ll see what I can do! ⬇️

   [Parameter()]
        [ValidatePattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
        [string]$Email

This regular expression checks if the provided value matches the pattern used by email addresses. It verifies that the part before the @ can contain characters and numbers, and does the same for the part after the @.

Finally, after the . symbol, it expects characters to define the domain and potential top-level domain.

There are many patterns you can explore to match your specific business case, ensuring you don’t receive invalid input! 😉

If you run the script with the parameters, you’ll see that it fails appropriately if the input doesn’t match the expected pattern.

$userManagement.users | ForEach-Object {
    $fetchedUser = $_ | Set-User -Aging 30 -verbose -Email "info "
    $fetchedUser;
}

And of course providing a valid value results in a match!

$userManagement.users | ForEach-Object {
    $fetchedUser = $_ | Set-User -Aging 30 -verbose -Email "info@bartpasmans.tech"
    $fetchedUser;
}

Script validation

The last attribute I’ll highlight in this post is the ValidateScript attribute. This one is, at least for me, an often-overlooked gem in the world of scripting!

The ValidateScript attribute runs custom code on the provided variables, allowing you to perform additional checks, enhance readability, and much more. For me, it’s one of the most powerful attributes available in PowerShell.

Let me show you how you can leverage this in practice!

First, we’ll add a new parameter:

  [parameter()]
  [ValidateScript({Get-Employee -EmployeeNumber $_})]
  [int]$EmployeeNumber

And introduce the new function

function Get-Employee {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [int]$EmployeeNumber
    )

    begin {
        Write-Host "Setting up the CheckEmployee function"
    }

    process {
        if(0 -eq $EmployeeNumber) {
            throw "Employee number is required"     
        } 

        if ($EmployeeNumber -eq 1) {
            return $true  
        }
        return $false  
    }
}

This is a simple example: if the employeeNumber is 1, the function returns true, indicating that the conditions are met, and everything is good to go!

However, if we provide a value that doesn’t match the expected criteria, the code will fail:

You can extend this approach to, for example, API calls connecting to other systems, creating a network of functions that guide your code through the business logic.

Summary

In this post, we’ve explored how to create more advanced functions, enriching them with various attributes to elevate the quality of your code. We’ve discussed ways to validate input, use regular expressions for pattern matching, and ensure that your script handles different conditions effectively. All of these techniques will not only make your life easier but also result in cleaner, more maintainable code in the long run. This will improve the readability and sustainability (resilience? of your scripts. 😉

We also touched on impact handlers remember? Sometimes it’s wise to ask the user for that final confirmation before making potentially irreversible changes, such as deleting or modifying important data.

And last but not least, don’t forget about my personal favorite: the ValidateScript attribute. When used correctly, it’s a powerful tool that helps you create a more interconnected, safe, and reliable automation framework for your functions.

I hope this post has helped you level up your scripting skills! Stay tuned for the next post, where we’ll focus on bringing everything together and setting up libraries for sharing the code we create with others. Stay in touch! 😉

#ScriptingLikeAPro #BeAScriptingPro 🚀🚀🚀🚀

To help you out, below you can find the full code used for this post 😉 Happy scripting!

class User {
    [string]$userName

    [int]$age

    User([string]$UserName, [int]$Age) {
        $this.userName = $UserName
        $this.age = $Age
    }
}

class UserManagement {
    [System.Collections.Generic.List[User]]$users = @()

    UserManagement() {
        $this.users.Add((New-Object User -ArgumentList "John", 25))
        $this.users.Add((New-Object User -ArgumentList "Jane", 30))
    }
}

[UserManagement]$userManagement = New-Object UserManagement

function Get-Employee {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [int]$EmployeeNumber
    )

    begin {
        Write-Host "Setting up the CheckEmployee function"
    }

    process {
        if(0 -eq $EmployeeNumber) {
            throw "Employee number is required"     
        } 

        if ($EmployeeNumber -eq 1) {
            return $true  
        }
        return $false  
    }
}


function Set-User {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    param (       
        [Parameter(Mandatory=$true, Position = 0, HelpMessage = "The input [User] object", ValueFromPipeline=$true)]
        [User]$User,

        [Parameter()] 
        [ValidateSet(25, 30, 35)]
        [Alias("Aged", "Aging")]
        [int]$Age,

        [Parameter()]
        [switch]$ConvertToJSON,

        [Parameter()]
        [ValidatePattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
        [string]$Email,

        [parameter()]
        [ValidateScript({Get-Employee -EmployeeNumber $_})]
        [int]$EmployeeNumber
    )

    dynamicparam {
        # Create the dictionary to hold dynamic parameters
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()

        # Since $ConvertToJSON isn't available in dynamicparam, check if it's passed via the command line
        if ($MyInvocation.BoundParameters.ContainsKey('ConvertToJSON')) {
            # Define the parameter attributes
            $paramAttribute = [System.Management.Automation.ParameterAttribute]@{
                Mandatory = $false
            }

            # Create a collection of attributes for the dynamic parameter
            $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
            $attributeCollection.Add($paramAttribute)

            # Create the dynamic parameter (name, type, attributes)
            $dynParam = [System.Management.Automation.RuntimeDefinedParameter]::new(
                'ManipulateAge', [string], $attributeCollection
            )

            # Store it in the dictionary
            $paramDictionary.Add('ManipulateAge', $dynParam)
        }
        return $paramDictionary
    }

    begin {
        write-verbose "Setting up the Set-User function"
    }

    process {         
        if($PSCmdlet.ShouldProcess($User.userName, "Setting Age to $Age")) {
            $User = $User | Set-UserAge -Age $Age
        } else {
            Write-Host "Skipping the user: $($User.userName)"
        }
    }

    end {
        # Retrieve the dynamic parameter value
        $boundParams = $PSBoundParameters
        $extraInfo = if ($boundParams.ContainsKey('ManipulateAge')) { $boundParams['ManipulateAge'] } else { "None" }

        Write-Host "EnableExtra: $ManipulateAge"
        Write-Host "ExtraInfo: $extraInfo"

        if($ConvertToJSON) {
            return $User | ConvertTo-Json
        }
        return $User
    }
}

function Set-UserAge {
    [CmdletBinding()]
    param (       
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [User]$User,

        [Parameter()]        
        [int]$Age
    )

    begin {
        write-verbose "Setting up the Set-UserAge function"
    }

    process {
        if(0 -ne $age -and $age -ge $User.age  ) {
            $User.age = $Age
        }        
    }

    end {
        return $User
    }
}

$userManagement.users | ForEach-Object {
    $fetchedUser = $_ | Set-User -Aging 30 -verbose -Email "info@bartpasmans.tech" -EmployeeNumber 1
    $fetchedUser;
}

Leave a Reply

Your email address will not be published. Required fields are marked *