Recently I was involved in an exchange on Twitter. The discussion originally was about find a way to identify what cmdlets are available for a given version of PowerShell or operating system. But after a little back and forth, I discovered the real issue: how to identify what cmdlets or modules that are necessary to run a script. The issue for my tweet-pal centered on Desired State Configuration (DSC). He was having issues using some DSC resources and it was most likely due to missing cmdlets on the target node. A DSC resource is packaged as a module, which means it should be possible to look through the psm1 file and identify what cmdlets and modules it requires. Here are a few ways to approach this problem, and by approach I mean letting PowerShell do the work for us.
The first approach is a simple brute-force technique using regular expressions. Most DSC resources will be getting or setting something. Assuming the module author is using full cmdlet names, I can define a regular expression pattern to search for text that looks like a cmdlet name.
[regex]$rx="b(Get|New|Set|Add|Remove|Test)-w+b"
The pattern looks for a word that starts with Get, New or any of the rest, followed by a dash and then any other word. To search, we need to identify the path to the DSC module file.
$dsc = Get-DscResource -Name xIPAddress
The path property will be to the module file. I’ll get the module contents.
$code = Get-content $dsc.Path
And then I’ll use the regex object to find all matching values.
$rx.matches($code).Value | Sort | Get-Unique
Because there will most likely be duplicates, I want a unique list. I have that found sorting works best for this. But now I can see what cmdlets are used in this resource.
I could then test for that command on my target computer.
invoke-command {get-command get-netipaddress} -computername Server01
Or perhaps test for all of them.
$cmdlets = $rx.matches($code).Value | Sort | Get-Unique | where {$_ -notmatch "-TargetResource$"}
invoke-command {get-command $using:cmdlets} -computername Server01
I filtered out the commands that are part of every DSC resource. Now I can easily see if there are any potential problems before I deploy my configuration. If you like this approach, then you can use a function that simplifies the process.
#requires -version 4.0
Function Get-DSCResourceCommands {
[cmdletbinding()]
Param([string]$Name)
Begin {
Write-Verbose -Message "Starting $($MyInvocation.Mycommand)"
#define a regular expression to pull out cmdlet names using some common verbs
[regex]$rx="b(Get|New|Set|Add|Remove|Test)-w+b"
} #begin
Process {
Write-Verbose "Getting DSC Resource $Name"
Try {
$resource = Get-DscResource -Name $name -ErrorAction Stop
Write-Verbose ($resource | out-string)
}
Catch {
Throw
}
if ($resource) {
#get the code from the module path which will be something like this:
#'C:Program FilesWindowsPowerShellModulesxSmbShareDSCResourcesMSFT_xSmbShareMSFT_xSmbShare.psm1'
Write-Verbose "Processing content from $($resource.path)"
$code = Get-Content -path $resource.path
#find matching names
$rx.matches($code).Value | sort | Get-Unique | Where {$_ -notmatch "-TargetResource$"}
} #if $resource
} #process
End {
Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
} #end
}
All you need to do is enter the name of a DSC resource.
Get-DSCResourceCommands -Name xrdremoteapp
One drawback to this approach is that you might also identify internally defined functions that match the regular expression pattern. There’s nothing wrong with functions as they are most likely calling regular PowerShell cmdlets. But another technique would be to parse the script file using PowerShell’s AST (abstract syntax tree).
Without getting too deep into the technical .NET details, you can use the AST to analyze a block of PowerShell code. The AST will parse the commands and create a series of tokens. These tokens represent parts of a PowerShell command. Using the AST is not something I would expect of a typical IT pro, so I ‘ll give you a hand and offer function called Test-ScriptFile:
#requires -version 3.0
Function Test-ScriptFile {
<#
.Synopsis
Test a PowerShell script for cmdlets
.Description
This command will analyze a PowerShell script file and display a list of detected commands such as PowerShell cmdlets and functions. Commands will be compared to what is installed locally. It is recommended you run this on a Windows 8.1 client with the latest version of RSAT installed. Unknown commands could also be internally defined functions. If in doubt view the contents of the script file in the PowerShell ISE or a script editor.
You can test any .ps1, .psm1 or .txt file.
.Parameter Path
The path to the PowerShell script file. You can test any .ps1, .psm1 or .txt file.
.Parameter UnknownOnly
Only display commands that could not be resolved based on locally installed modules.
.Example
PS C:> test-scriptfile C:scriptsRemove-MyVM2.ps1
CommandType Name ModuleName
----------- ---- ----------
Cmdlet Disable-VMEventing Hyper-V
Cmdlet ForEach-Object Microsoft.PowerShell.Core
Cmdlet Get-VHD Hyper-V
Cmdlet Get-VMSnapshot Hyper-V
Cmdlet Invoke-Command Microsoft.PowerShell.Core
Cmdlet New-PSSession Microsoft.PowerShell.Core
Cmdlet Out-Null Microsoft.PowerShell.Core
Cmdlet Out-String Microsoft.PowerShell.Utility
Cmdlet Remove-Item Microsoft.PowerShell.Management
Cmdlet Remove-PSSession Microsoft.PowerShell.Core
Cmdlet Remove-VM Hyper-V
Cmdlet Remove-VMSnapshot Hyper-V
Cmdlet Write-Debug Microsoft.PowerShell.Utility
Cmdlet Write-Verbose Microsoft.PowerShell.Utility
Cmdlet Write-Warning Microsoft.PowerShell.Utility
.Example
PS C:> get-dscresource xJeaToolkit | Test-ScriptFile | Sort CommandType | format-table
CommandType Name ModuleName
----------- ---- ----------
Cmdlet Join-Path Microsoft.PowerShell.Management
Cmdlet Import-Module Microsoft.PowerShell.Core
Cmdlet Write-Verbose Microsoft.PowerShell.Utility
Cmdlet Out-String Microsoft.PowerShell.Utility
Cmdlet Write-Debug Microsoft.PowerShell.Utility
Cmdlet Test-Path Microsoft.PowerShell.Management
Cmdlet Remove-Module Microsoft.PowerShell.Core
Cmdlet Get-Module Microsoft.PowerShell.Core
Cmdlet Export-ModuleMember Microsoft.PowerShell.Core
Cmdlet Get-Content Microsoft.PowerShell.Management
Cmdlet Format-List Microsoft.PowerShell.Utility
Unknown Assert-JeaDirectory Unknown
Unknown Export-JEAProxy Unknown
Unknown Get-JeaDir Unknown
Unknown New-TerminatingError Unknown
Unknown Get-JeaToolKitDir Unknown
.Example
PS C:> get-dscresource cvhdfile | test-scriptfile -UnknownOnly
CommandType Name ModuleName
----------- ---- ----------
Unknown EnsureVHDState Unknown
Unknown GetItemToCopy Unknown
Unknown SetVHDFile Unknown
.Notes
Last Updated: November 2, 2014
Version : 1.0
Learn more about PowerShell:
Essential PowerShell Learning Resources
****************************************************************
* DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED *
* THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF *
* YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, *
* DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. *
****************************************************************
.Link
Get-Command
Get-Alias
#>
[cmdletbinding()]
Param(
[Parameter(Position = 0,Mandatory = $True,HelpMessage = "Enter the path to a PowerShell script file,",
ValueFromPipeline = $True,ValueFromPipelineByPropertyName = $True)]
[ValidatePattern( ".(ps1|psm1|txt)$")]
[ValidateScript({Test-Path $_ })]
[string]$Path,
[switch]$UnknownOnly
)
Begin {
Write-Verbose "Starting $($MyInvocation.Mycommand)"
Write-Verbose "Defining AST variables"
New-Variable astTokens -force
New-Variable astErr -force
} #begin
Process {
Write-Verbose "Parsing $path"
$AST = [System.Management.Automation.Language.Parser]::ParseFile($Path,[ref]$astTokens,[ref]$astErr)
#group tokens and turn into a hashtable
$h = $astTokens | Group-Object tokenflags -AsHashTable -AsString
$commandData = $h.CommandName | where {$_.text -notmatch "-TargetResource$"} |
foreach {
Write-Verbose "Processing $($_.text)"
Try {
$cmd = $_.Text
$resolved = $cmd | get-command -ErrorAction Stop
if ($resolved.CommandType -eq 'Alias') {
Write-Verbose "Resolving an alias"
#manually handle "?" because Get-Command and Get-Alias won't.
Write-Verbose "Detected the Where-Object alias '?'"
if ($cmd -eq '?'){
Get-Command Where-Object
}
else {
$resolved.ResolvedCommandName | Get-Command
}
}
else {
$resolved
}
} #Try
Catch {
Write-Verbose "Command is not recognized"
#create a custom object for unknown commands
[PSCustomobject]@{
CommandType = "Unknown"
Name = $cmd
ModuleName = "Unknown"
} #custom object
} #catch
} #foreach
if ($UnknownOnly) {
Write-Verbose "Filtering for unknown commands only"
$commandData = $commandData | where {$_.Commandtype -eq 'Unknown'}
}
else {
Write-Verbose "Displaying all commands"
}
#display results
$commandData | Sort-Object -property Name | Select-Object -property CommandType,Name,ModuleName -Unique
} #process
End {
Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
} #end
} #end function
The script has comment-based help and hopefully plenty of inline comments, so I won’t repeat those here. To use, all you need to do is feed it the path to a script file. This can be any .ps1, .psm1 or even a .txt file. Fortunately, the DSC resource object’s Path property is conveniently named and I wrote Test-ScriptFile to take advantage of it.
get-dscresource xazurevm | test-scriptfile
The function not only identifies commands, it will resolve aliases and get command information, if it can be found locally. I recommend having a Windows 8.1 desktop with RSAT installed to get the most coverage. But as you can see from the screenshot, sometimes even that might not be enough.
Some of these commands may be internal functions, but at least I have some command names to research before using this resource in a configuration. Let me also point out that you can use this function for any script file, not just a DSC resource. This is a handy way of identifying what commands are in that script you just downloaded.
I also added parameter to only display unknown items.
Now I at least know what to search for when I open the file in my script editor.
These are a few ways you can identify what commands will be run in a PowerShell script. But the best technique is to review the script file, DSC Resources included, in a scripting editor before you begin using it.