Table of Contents
- 1 Introduction
- 2 CmdletBinding
- 3 Function Structure (Begin, process, end)
- 4 Function Pipeline
- 5 Function Parameter Options
- 6 PowerShell Modules
- 7 Function Documentation
- 8 Convenient Essentials
- 8.1 Approved Verbs
- 8.2 Check PowerShell Module Paths
- 8.3 Global variables
- 8.4 Switch Case and Enums
- 8.5 Check PowerShell version
- 8.6 Load ps1 file using $PSScriptRoot
- 8.7 Collect, Check, Overwrite and Pass Function Parameters
- 8.8 Common Data Types
- 8.9 Typed Dictionary
- 8.10 Escaping characters
- 8.11 Subexpression
- 8.12 ScriptBlock & Call operator
- 8.13 Classes
- 8.14 Custom Object
- 8.15 Hash Table
- 8.16 Loosen Execution Policy
- 9 Boilerplate PowerShell Function
- 10 Conclusion
Introduction
There is a lot to read about cmdlets in the official documentation. To put it simple, a cmdlet (“command-let”) is a command to perform a specific function and is written in C# and compiled as a .NET class. Examples of cmdlets are the usual commands like Get-Verb and Get-ChildItem. A cmdlet has some advantages over a regular PowerShell function, so in behavior it is basically a function with extra features. You can mimic this cmdlet behavior in a PowerShell written function by using CmdletBinding(). In this post I will try to refresh knowledge about cmdlets and functions in PowerShell.
All given examples are for demonstration purposes and all are considered to be *.ps1 files unless Export-ModuleMember is used.
CmdletBinding
You can make a PowerShell function behave as a compiled cmdlet using [CmdletBinding()]. This enhances the function in the following ways:
- Supports parameters like -Verbose and -ErrorAction automatically.
- Supports Pipeline input, this allows functions to process input objects like cmdlets.
- Enables confirmation prompts such as ShouldProcess which adds the parameters -Confirm and -WhatIf
- Improved parameter handling, like PositionalBinding.
Example
# Finds first directory with provided name, returns full path when found and stops searching immediately
function Find-Directory {
[CmdletBinding(SupportsShouldProcess=$True)]
param(
[Parameter(Mandatory=$True)]
[String]$Name
)
Get-ChildItem -Recurse -Directory | Where-Object { $_.Name -eq $Name } | ForEach-Object {
Write-Output $_.FullName
break
}
}
Find-Directory -Name "Pictures" -Verbose
PowerShellFunction Structure (Begin, process, end)
<#
.SYNOPSIS
Outputs list of found full paths of directories with specified Name
.PARAMETER Name
The name of the directory to search for
#>
function Find-Directory {
[CmdletBinding(SupportsShouldProcess=$True)]
param(
[Parameter(Mandatory=$True)]
[String]$Name
)
# Executes once before PROCESS
BEGIN {
$List = [System.Collections.ArrayList]@()
}
# Executes for each pipeline input
PROCESS {
Get-ChildItem -Recurse -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -eq $Name } |
ForEach-Object {
[void]$List.Add($_.FullName)
}
}
# Executes once after PROCESS
END {
foreach($Item in $List) {
Write-Output $Item
}
}
}
Find-Directory -Name "config" -Verbose
PowerShellFunction Pipeline
You can pass objects from one cmdlet or function to another using the pipe character: |
In this way, the output of one function can become the input of another.
To make this accept pipeline input we can alter the script:
<#
.SYNOPSIS
Outputs list of found full paths of directories with specified Name
.PARAMETER Name
The name of the directory to search for
#>
function Find-Directory {
[CmdletBinding(SupportsShouldProcess=$True)]
param(
[Parameter(Mandatory=$True, ValueFromPipeLine=$True)]
[String[]]$Name
)
# Executes once before PROCESS
BEGIN {
$List = [System.Collections.ArrayList]@()
}
# Executes for each pipeline input
PROCESS {
Get-ChildItem -Recurse -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -eq $Name } |
ForEach-Object {
[void]$List.Add($_.FullName)
}
}
# Executes once after PROCESS
END {
foreach($Item in $List) {
Write-Output $Item
}
}
}
"logs", "config" | Find-Directory
PowerShellFunction Parameter Options
Option | Description |
Mandatory | Makes the parameter mandatory ($true or $false) |
Position | Give a parameter a position so you can omit explicit specification of parameters. Defaults to true. |
ValueFromPipeline | Accepts pipeline variables. Demonstrated in Function Pipeline. |
ValueFromPipeLineByPropertyName | Accepts pipeline only when property name is set in the passed object. |
ValueFromRemainingArguments | Collects remaining parameters and stores them in this parameter name. |
ParameterSetName | Define a set of parameter names in a function. Only one set can be active at a time. |
HelpMessage | Add a help message to a parameter. |
DontShow | Hide parameter from both help and tab completion. You can still use the parameter. Set with $true or $false. |
Positional Parameters
<#
.SYNOPSIS
Find directory by Name and Path
Path may be omitted, then default of C:\ is used
Outputs each full path with directory name
#>
function Find-Directory {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True, Position=0)]
[String]$Name,
[Parameter(Mandatory=$False, Position=1)]
[String]$Path
)
# Checks if parameter is given
if (! $PSBoundParameters.ContainsKey('Path')) {
$Path = "C:\"
}
Get-ChildItem -Recurse -Directory -Path $Path -ErrorAction SilentlyContinue |
Where-Object { $_.Name -eq $Name } |
ForEach-Object {
Write-Host $_.FullName
}
}
Find-Directory "config" "C:\Users"
Find-Directory "bin"
PowerShellValue From Remaining Arguments
# outputs: 1 2 3
function Find-Directory {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)]
[String]$Name,
[Parameter(Mandatory=$False)]
[String]$Path,
[Parameter(ValueFromRemainingArguments=$True)]
$Rest
)
Write-Host $Rest
}
Find-Directory "config" "C:\Users" "1" "2" "3"
PowerShellPowerShell Modules
A PowerShell module has a different file extension: *.psm1
Each function of a Module needs to be explicitly exported, if you want to be able to call the function.
Modules will auto-load when inside a $env:PSModulePath directory and the *.psm1 filename must be the same as the directory name.
If you have altered the contents of an auto-loaded module, be sure to reload the PowerShell terminal window.
function Find-Directory {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)]
[String]$Name
)
# No implementation here
Write-Host $Name
}
Export-ModuleMember -Function Find-Directory
PowerShellA module cannot be used without importing it first: Import-Module Find-Directory
Function Documentation
<#
.SYNOPSIS
A brief description of the cmdlet or function.
.DESCRIPTION
A detailed explanation of what the cmdlet does.
.PARAMETER Name
Describes a specific parameter and its purpose.
.PARAMETER AnotherParameterName
Describes a specific parameter and its purpose.
.EXAMPLE
Provides usage examples with expected output.
.INPUTS
Specifies the types of objects that can be piped into the cmdlet.
.OUTPUTS
Defines the type of objects the cmdlet returns.
.NOTES
Additional information or special considerations.
.LINK
References related topics or external documentation.
.COMPONENT
Identifies the technology or feature the cmdlet is related to.
.ROLE
Specifies the user role relevant to the cmdlet.
.FUNCTIONALITY
Describes the cmdlet’s intended functionality.
.EXTERNALHELP
Links to an external help file instead of using comment-based help.
#>
PowerShellConvenient Essentials
Approved Verbs
The official documentation recommends to use approved verbs.
Get-Verb | Format-Wide -Column 6
PowerShellCheck PowerShell Module Paths
$env:PSModulePath.Split(";")
PowerShellGlobal variables
$global:GlobalVarName = "Data"
PowerShellSwitch Case and Enums
Calling Test-Module will output Import chosen followed by Remove chosen.
enum ModuleInvocation {
Import
Remove
}
function Test-PS {
[CmdletBinding()]
param(
[Parameter(Mandatory=$True)]
[ModuleInvocation]$ModuleInvocation
)
switch($ModuleInvocation) {
Import {
Write-Host "Import chosen"
}
Remove {
Write-Host "Remove chosen"
}
}
}
function Test-Module {
Test-PS -ModuleInvocation ([ModuleInvocation]::Import)
Test-PS -ModuleInvocation ([ModuleInvocation]::Remove)
}
Export-ModuleMember -Function Test-PS
Export-ModuleMember -Function Test-Module
PowerShellCheck PowerShell version
$PSVersionTable.PSVersion
PowerShellLoad ps1 file using $PSScriptRoot
A *.ps1 file is not a module. You can use/access the contents (e.g. functions) of the ps1 file by just calling it once from another PowerShell file:
.$PSScriptRoot\Install_Winget.ps1
PowerShell$PSScriptRoot is the absolute path to the current directory.
$PSCommandPath is the absolute path to the current file.
Collect, Check, Overwrite and Pass Function Parameters
function Test-PS {
[CmdletBinding()]
param(
[string]$Path, # could as well be initialized directly offcourse
[string]$Filter
)
$DefaultParams = @{
Path = "C:\"
Filter = "*.log"
}
if ($PSBoundParameters.ContainsKey('Path')) {
$DefaultParams['Path'] = $Path
}
if ($PSBoundParameters.ContainsKey('Filter')) {
$DefaultParams['Filter'] = $Filter
}
Get-ChildItem @DefaultParams
}
Export-ModuleMember -Function Test-PS
PowerShellCommon Data Types
[string] | [bool] | [float] | @{ Variable = ”} # hash table |
[char] | [decimal] | [datetime] | @() # array, add with .Add() |
[byte] | [double] | [<datatype>[]] # typed array | [System.Collections.ArrayList]::new() |
[int] | [array] | [regex] | |
[long] | [xml] |
Typed Dictionary
$Dict = [System.Collections.Generic.Dictionary[string,int]]::new()
$Dict['x'] = 100
PowerShellEscaping characters
Write-Host "This is a `"quote`" inside quotes."
# `n = newline
# `r = carriage return
# `t = tab
PowerShellSubexpression
To evaluate variables inline.
Write-Host "Today it is $((Get-Date).DayOfWeek)."
PowerShellScriptBlock & Call operator
You can store PowerShell in a variable and invoke it later. It can also be passed remotely. A ScriptBlock can capture variables from its parent scope as well.
function Test-PS {
$ScriptBlock = {
param($name)
Write-Host "Hello $($name)"
}
& $ScriptBlock "Bob" # Invoke with '&' call operator
$ScriptBlock.Invoke("Mary") # Invoke
}
Export-ModuleMember -Function Test-PS
PowerShellClasses
function Test-PS {
$Person = [Person]::new("Bob", 40)
Write-Host $Person # Outputs: Bob. age 40
}
Export-ModuleMember -Function Test-PS
class Person {
[string]$FirstName
[int] $Age
# Constructor
Person([string]$fn, [int]$age) {
$this.FirstName = $fn
$this.Age = $age
}
[string] ToString() {
return "$($this.FirstName), age $($this.Age)"
}
}
PowerShellCustom Object
$Person = [PSCustomObject]@{
Name = 'T'
Age = 40
}
PowerShellHash Table
$Hata = @{
Name = 'T'
Age = 40
}
$Hata['Name'] # "T"
$Hata.Age # 40
$Hata['Default'] = 'default' # add new key
PowerShellLoosen Execution Policy
To loosen the execution policy:
Set-ExecutionPolicy -ExecutionPolicy Unrestricted
PowerShellBoilerplate PowerShell Function
This script does nothing, but can be used as a quick boilerplate to write new functions.
<#
.SYNOPSIS
Write here
.DESCRIPTION
.PARAMETER Name
.EXAMPLE
.INPUTS
.OUTPUTS
.NOTES
.LINK
.COMPONENT
.ROLE
.FUNCTIONALITY
.EXTERNALHELP
#>
function Select-FunctionName {
[CmdletBinding(SupportsShouldProcess=$True)]
param(
[Parameter(Mandatory=$True)]
[string]$Name,
[Parameter(ValueFromRemainingArguments=$True)]
$Remaining
)
BEGIN {
}
PROCESS {
try {
}
catch {
Write-Host $_.ScriptStackTrace
}
finally {
}
}
END {
}
}
Export-ModuleMember -Function Select-FunctionName
PowerShellConclusion
Usually, I don’t use PowerShell that often. Sometimes several months pass and I have to remember a lot of the basic syntax and practices. With this post I hope to give a reference to quickly refresh those basics I tend to forget. When creating functions, we should stick to the best practices and use the approved verbs for creating function names.