Fast Registry Searcher in Powershell - powershell

I'm trying to incorporate this search-registry script that I found on Github into one of my scripts.
https://github.com/KurtDeGreeff/PlayPowershell/blob/master/Search-Registry.ps1
To test it, I used one of the examples that it provided:
Search-Registry -StartKey HKLM -Pattern $ENV:USERNAME -MatchData
After executing, I was still prompted to enter a StartKey and a Pattern into the console. After providing that information once again, the command fails.
cmdlet Search-Registry.ps1 at command pipeline position 1
Supply values for the following parameters:
StartKey: HKLM
Pattern: $ENV:USERNAME -MatchData
You must specify at least one of: -MatchKey -MatchValue -MatchData
At C:\Users\Cole\Desktop\Powershell\Search-Registry.ps1:93 char:5
+ throw "You must specify at least one of: -MatchKey -MatchValue -M ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (You must specif...alue -MatchData:String) [], RuntimeException
+ FullyQualifiedErrorId : You must specify at least one of: -MatchKey -MatchValue -MatchData
Is there something that I'm doing incorrectly?

I have tested that same function on GitHub too and also came across errors with it, so I decided to do a complete rewrite myself.
The function below can use either Regular Expression matches or Wildcard comparisons using the -like operator.
To search for nameless default properties, either don't specify the Pattern or RegexPattern parameters at all, or use one of them with an empty string.
The function also has a Recurse switch, so it is up to you if you want to recursively search through all subkeys or not.
Note that although the function performs fast, searching the registry can take a long time to finish..
# If you want to run this on PowerShell < 3.0 use
# New-Object -TypeName PSObject -Property #{ ... } wherever it says [PSCustomObject]#{ ... }
# and change the -version value for 'requires' to 2
#requires -version 3
function Search-Registry {
<#
.SYNOPSIS
Searches the registry on one or more computers for a specified text pattern.
.DESCRIPTION
Searches the registry on one or more computers for a specified text pattern.
Supports searching for any combination of key names, value names, and/or value data.
The search pattern is either a regular expression or a wildcard pattern using the 'like' operator.
(both are case-insensitive)
.PARAMETER ComputerName
(Required) Searches the registry on the specified computer(s). This parameter supports piped input.
.PARAMETER Pattern
(Optional) Searches using a wildcard pattern and the -like operator.
Mutually exclusive with parameter 'RegexPattern'
.PARAMETER RegexPattern
(Optional) Searches using a regular expression pattern.
Mutually exclusive with parameter 'Pattern'
.PARAMETER Hive
(Optional) The registry hive rootname.
Can be any of 'HKEY_CLASSES_ROOT','HKEY_CURRENT_CONFIG','HKEY_CURRENT_USER','HKEY_DYN_DATA','HKEY_LOCAL_MACHINE',
'HKEY_PERFORMANCE_DATA','HKEY_USERS','HKCR','HKCC','HKCU','HKDD','HKLM','HKPD','HKU'
If not specified, the hive must be part of the 'KeyPath' parameter.
.PARAMETER KeyPath
(Optional) Starts the search at the specified registry key. The key name contains only the subkey.
This parameter can be prefixed with the hive name.
In that case, parameter 'Hive' is ignored as it is then taken from the given path.
Examples:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall
HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall
Software\Microsoft\Windows\CurrentVersion\Uninstall
.PARAMETER MaximumResults
(Optional) Specifies the maximum number of results per computer searched.
A value <= 0 means will return the maximum number of possible matches (2147483647).
.PARAMETER SearchKeyName
(Optional) Searches for registry key names. You must specify at least one of -SearchKeyName, -SearchPropertyName, or -SearchPropertyValue.
.PARAMETER SearchPropertyName
(Optional) Searches for registry value names. You must specify at least one of -SearchKeyName, -SearchPropertyName, or -SearchPropertyValue.
.PARAMETER SearchPropertyValue
(Optional) Searches for registry value data. You must specify at least one of -SearchKeyName, -SearchPropertyName, or -SearchPropertyValue.
.PARAMETER Recurse
(Optional) If set, the function will recurse the search through all subkeys found.
.OUTPUTS
PSCustomObjects with the following properties:
ComputerName The computer name where the search was executed
Hive The hive name used in Win32 format ("CurrentUser", "LocalMachine" etc)
HiveName The hive name used ("HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE" etc.)
HiveShortName The abbreviated hive name used ("HKCU", "HKLM" etc.)
Path The full registry path ("HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall")
SubKey The subkey without the hive ("Software\Microsoft\Windows\CurrentVersion\Uninstall")
ItemType Informational: describes the type 'RegistryKey' or 'RegistryProperty'
DataType The .REG formatted datatype ("REG_SZ", "REG_EXPAND_SZ", "REG_DWORD" etc.). $null for ItemType 'RegistryKey'
ValueKind The Win32 datatype ("String", "ExpandString", "DWord" etc.). $null for ItemType 'RegistryKey'
PropertyName The name of the property. $null for ItemType 'RegistryKey'
PropertyValue The value of the registry property. $null for ItemType 'RegistryKey'
PropertyValueRaw The raw, unexpanded value of the registry property. $null for ItemType 'RegistryKey'
The difference between 'PropertyValue' and 'PropertyValueRaw' is that in 'PropertyValue' Environment names are expanded
('%SystemRoot%' in the data gets expanded to 'C:\Windows'), whereas in 'PropertyValueRaw' the data is returned as-is.
(Environment names return as '%SystemRoot%')
.EXAMPLE
Search-Registry -Hive HKLM -KeyPath SOFTWARE -Pattern $env:USERNAME -SearchPropertyValue -Recurse -Verbose
Searches HKEY_LOCAL_MACHINE on the local computer for registry values whose data contains the current user's name.
Searches like this can take a long time and you may see warning messages on registry keys you are not allowed to enter.
.EXAMPLE
Search-Registry -KeyPath 'HKEY_CURRENT_USER\Printers\Settings' -Pattern * -SearchPropertyName | Export-Csv -Path 'D:\printers.csv' -NoTypeInformation
or
Search-Registry -Hive HKEY_CURRENT_USER -KeyPath 'Printers\Settings' -Pattern * -SearchPropertyName | Export-Csv -Path 'D:\printers.csv' -NoTypeInformation
Searches HKEY_CURRENT_USER (HKCU) on the local computer for printer names and outputs it as a CSV file.
.EXAMPLE
Search-Registry -KeyPath 'HKLM:\SOFTWARE\Classes\Installer' -Pattern LastUsedSource -SearchPropertyName -Recurse
or
Search-Registry -Hive HKLM -KeyPath 'SOFTWARE\Classes\Installer' -Pattern LastUsedSource -SearchPropertyName -Recurse
Outputs the LastUsedSource registry entries on the current computer.
.EXAMPLE
Search-Registry -KeyPath 'HKCR\.odt' -RegexPattern '.*' -SearchKeyName -MaximumResults 10 -Verbose
or
Search-Registry -Hive HKCR -KeyPath '.odt' -RegexPattern '.*' -SearchKeyName -MaximumResults 10 -Verbose
Outputs at most ten matches if the specified key exists.
This command returns a result if the current computer has a program registered to open files with the .odt extension.
The pattern '.*' means match everything.
.EXAMPLE
Get-Content Computers.txt | Search-Registry -KeyPath "HKLM:\SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine" -Pattern '*' -SearchPropertyName | Export-Csv -Path 'D:\powershell.csv' -NoTypeInformation
Searches for any property name in the registry on each computer listed in the file Computers.txt starting at the specified subkey.
Output is sent to the specified CSV file.
.EXAMPLE
Search-Registry -KeyPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace' -SearchPropertyName -Recurse -Verbose
Searches for the default (nameless) properties in the specified registry key.
#>
[CmdletBinding(DefaultParameterSetName = 'ByWildCard')]
Param(
[Parameter(ValueFromPipeline = $true, Mandatory = $false, Position = 0)]
[string[]]$ComputerName = $env:COMPUTERNAME,
[Parameter(Mandatory = $false, ParameterSetName = 'ByRegex')]
[string]$RegexPattern,
[Parameter(Mandatory = $false, ParameterSetName = 'ByWildCard')]
[string]$Pattern,
[Parameter(Mandatory = $false)]
[ValidateSet('HKEY_CLASSES_ROOT','HKEY_CURRENT_CONFIG','HKEY_CURRENT_USER','HKEY_DYN_DATA','HKEY_LOCAL_MACHINE',
'HKEY_PERFORMANCE_DATA','HKEY_USERS','HKCR','HKCC','HKCU','HKDD','HKLM','HKPD','HKU')]
[string]$Hive,
[string]$KeyPath,
[int32] $MaximumResults = [int32]::MaxValue,
[switch]$SearchKeyName,
[switch]$SearchPropertyName,
[switch]$SearchPropertyValue,
[switch]$Recurse
)
Begin {
# detect if the function is called using the pipeline or not
# see: https://communary.net/2015/01/12/quick-tip-determine-if-input-comes-from-the-pipeline-or-not/
# and: https://www.petri.com/unraveling-mystery-myinvocation
[bool]$isPipeLine = $MyInvocation.ExpectingInput
# sanitize given parameters
if ([string]::IsNullOrWhiteSpace($ComputerName) -or $ComputerName -eq '.') { $ComputerName = $env:COMPUTERNAME }
# parse the give KeyPath
if ($KeyPath -match '^(HK(?:CR|CU|LM|U|PD|CC|DD)|HKEY_[A-Z_]+)[:\\]?') {
$Hive = $matches[1]
# remove HKLM, HKEY_CURRENT_USER etc. from the path
$KeyPath = $KeyPath.Split("\", 2)[1]
}
switch($Hive) {
{ #('HKCC', 'HKEY_CURRENT_CONFIG') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::CurrentConfig; break }
{ #('HKCR', 'HKEY_CLASSES_ROOT') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::ClassesRoot; break }
{ #('HKCU', 'HKEY_CURRENT_USER') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::CurrentUser; break }
{ #('HKDD', 'HKEY_DYN_DATA') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::DynData; break }
{ #('HKLM', 'HKEY_LOCAL_MACHINE') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::LocalMachine; break }
{ #('HKPD', 'HKEY_PERFORMANCE_DATA') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::PerformanceData; break }
{ #('HKU', 'HKEY_USERS') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::Users; break }
}
# critical: Hive could not be determined
if (!$objHive) {
Throw "Parameter 'Hive' not specified or could not be parsed from the 'KeyPath' parameter."
}
# critical: no search criteria given
if (-not ($SearchKeyName -or $SearchPropertyName -or $SearchPropertyValue)) {
Throw "You must specify at least one of these parameters: 'SearchKeyName', 'SearchPropertyName' or 'SearchPropertyValue'"
}
# no patterns given will only work for SearchPropertyName and SearchPropertyValue
if ([string]::IsNullOrEmpty($RegexPattern) -and [string]::IsNullOrEmpty($Pattern)) {
if ($SearchKeyName) {
Write-Warning "Both parameters 'RegexPattern' and 'Pattern' are emtpy strings. Searching for KeyNames will not yield results."
}
}
# create two variables for output purposes
switch ($objHive.ToString()) {
'CurrentConfig' { $hiveShort = 'HKCC'; $hiveName = 'HKEY_CURRENT_CONFIG' }
'ClassesRoot' { $hiveShort = 'HKCR'; $hiveName = 'HKEY_CLASSES_ROOT' }
'CurrentUser' { $hiveShort = 'HKCU'; $hiveName = 'HKEY_CURRENT_USER' }
'DynData' { $hiveShort = 'HKDD'; $hiveName = 'HKEY_DYN_DATA' }
'LocalMachine' { $hiveShort = 'HKLM'; $hiveName = 'HKEY_LOCAL_MACHINE' }
'PerformanceData' { $hiveShort = 'HKPD'; $hiveName = 'HKEY_PERFORMANCE_DATA' }
'Users' { $hiveShort = 'HKU' ; $hiveName = 'HKEY_USERS' }
}
if ($MaximumResults -le 0) { $MaximumResults = [int32]::MaxValue }
$script:resultCount = 0
[bool]$useRegEx = ($PSCmdlet.ParameterSetName -eq 'ByRegex')
# -------------------------------------------------------------------------------------
# Nested helper function to (recursively) search the registry
# -------------------------------------------------------------------------------------
function _RegSearch([Microsoft.Win32.RegistryKey]$objRootKey, [string]$regPath, [string]$computer) {
try {
if ([string]::IsNullOrWhiteSpace($regPath)) {
$objSubKey = $objRootKey
}
else {
$regPath = $regPath.TrimStart("\")
$objSubKey = $objRootKey.OpenSubKey($regPath, $false) # $false --> ReadOnly
}
}
catch {
Write-Warning ("Error opening $($objRootKey.Name)\$regPath" + "`r`n " + $_.Exception.Message)
return
}
$subKeys = $objSubKey.GetSubKeyNames()
# Search for Keyname
if ($SearchKeyName) {
foreach ($keyName in $subKeys) {
if ($script:resultCount -lt $MaximumResults) {
if ($useRegEx) { $isMatch = ($keyName -match $RegexPattern) }
else { $isMatch = ($keyName -like $Pattern) }
if ($isMatch) {
# for PowerShell < 3.0 use: New-Object -TypeName PSObject -Property #{ ... }
[PSCustomObject]#{
'ComputerName' = $computer
'Hive' = $objHive.ToString()
'HiveName' = $hiveName
'HiveShortName' = $hiveShort
'Path' = $objSubKey.Name
'SubKey' = "$regPath\$keyName".TrimStart("\")
'ItemType' = 'RegistryKey'
'DataType' = $null
'ValueKind' = $null
'PropertyName' = $null
'PropertyValue' = $null
'PropertyValueRaw' = $null
}
$script:resultCount++
}
}
}
}
# search for PropertyName and/or PropertyValue
if ($SearchPropertyName -or $SearchPropertyValue) {
foreach ($name in $objSubKey.GetValueNames()) {
if ($script:resultCount -lt $MaximumResults) {
$data = $objSubKey.GetValue($name)
$raw = $objSubKey.GetValue($name, '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if ($SearchPropertyName) {
if ($useRegEx) { $isMatch = ($name -match $RegexPattern) }
else { $isMatch = ($name -like $Pattern) }
}
else {
if ($useRegEx) { $isMatch = ($data -match $RegexPattern -or $raw -match $RegexPattern) }
else { $isMatch = ($data -like $Pattern -or $raw -like $Pattern) }
}
if ($isMatch) {
$kind = $objSubKey.GetValueKind($name).ToString()
switch ($kind) {
'Binary' { $dataType = 'REG_BINARY'; break }
'DWord' { $dataType = 'REG_DWORD'; break }
'ExpandString' { $dataType = 'REG_EXPAND_SZ'; break }
'MultiString' { $dataType = 'REG_MULTI_SZ'; break }
'QWord' { $dataType = 'REG_QWORD'; break }
'String' { $dataType = 'REG_SZ'; break }
default { $dataType = 'REG_NONE'; break }
}
# for PowerShell < 3.0 use: New-Object -TypeName PSObject -Property #{ ... }
[PSCustomObject]#{
'ComputerName' = $computer
'Hive' = $objHive.ToString()
'HiveName' = $hiveName
'HiveShortName' = $hiveShort
'Path' = $objSubKey.Name
'SubKey' = $regPath.TrimStart("\")
'ItemType' = 'RegistryProperty'
'DataType' = $dataType
'ValueKind' = $kind
'PropertyName' = if ([string]::IsNullOrEmpty($name)) { '(Default)' } else { $name }
'PropertyValue' = $data
'PropertyValueRaw' = $raw
}
$script:resultCount++
}
}
}
}
# recurse through all subkeys
if ($Recurse) {
foreach ($keyName in $subKeys) {
if ($script:resultCount -lt $MaximumResults) {
$newPath = "$regPath\$keyName"
_RegSearch $objRootKey $newPath $computer
}
}
}
# close opened subkey
if (($objSubKey) -and $objSubKey.Name -ne $objRootKey.Name) { $objSubKey.Close() }
}
}
Process{
if ($isPipeLine) { $ComputerName = #($_) }
$ComputerName | ForEach-Object {
Write-Verbose "Searching the registry on computer '$ComputerName'.."
try {
$rootKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($objHive, $_)
_RegSearch $rootKey $KeyPath $_
}
catch {
Write-Error "$($_.Exception.Message)"
}
finally {
if ($rootKey) { $rootKey.Close() }
}
}
Write-Verbose "All Done searching the registry. Found $($script:resultCount) results."
}
}
It returns a collection of objects with the following properties:
ComputerName : MYMACHINE
Hive : LocalMachine
HiveName : HKEY_LOCAL_MACHINE
HiveShortName : HKLM
Path : HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\{F3F5824C-AD58-4728-AF59-A1EBE3392799}
SubKey : SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\{F3F5824C-AD58-4728-AF59-A1EBE3392799}
ItemType : RegistryProperty
DataType : REG_SZ
ValueKind : String
PropertyName : (Default)
PropertyValue : Sticky Notes Namespace Extension for Windows Desktop Search
PropertyValueRaw : Sticky Notes Namespace Extension for Windows Desktop Search
The difference between PropertyValue and PropertyValueRaw is that in PropertyValue Environment names are expanded
('%SystemRoot%' in the data gets expanded to 'C:\Windows'), whereas in PropertyValueRaw the data is returned as-is.
(Environment names return as '%SystemRoot%')

I suspect it may have to do with the way you downloaded the file, and how you're calling it. Please try the following steps and see if that works for you.
Click the link to view the file in RAW format, or just click here
Right-click on the page and Save As... a ps1 file in your scripts folder. Note: Make sure you save it with the .ps1 extension and not as txt.
Open Powershell, browse to the directory, and call it as so...
.\Search-Registry.ps1 -StartKey HKCU -Pattern "Visual Studio" -MatchData
You can modify however you like, of course, I chose to look for Visual Studio in HKCU since I knew it would be a quick search.

Related

Powershell Global Variable usage as parameter to argument

$global:af_fp = "C:\Path\to\folder\"
Function function-name {
do things …
$global:af_fp = $global:af_fp + $variableFromDo_things + "_AF.csv"
}
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
Above is the generalized (and abbreviated) script contents for a powershell script.
Every time I run the script in this way, I get the following error:
Add-Content : Could not find a part of the path 'C:\Users\timeuser\Documents\'.
At C:\Users\timeuser\Documents\get_software.ps1:231 char:51
+ ... ware | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Users\timeuser\Documents\:String) [Add-Content], DirectoryNotFoundException
+ FullyQualifiedErrorId : GetContentWriterDirectoryNotFoundError,Microsoft.PowerShell.Commands.AddContentCommand
When I run
Get-Variable -Scope global
after running the script and seeing the error, the variable af_fp contains exactly the information I am seeking for the file name, however, the error shows the variable contents ending in ':String'.
To confuse me even more, if I comment out the lines containing '$global:...' and re-run the same script, IT ACTUALL RUNS AND SAVES THE FILE USING THE LINE
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
AS INTENDED. Of course, I had to run the script and watch it error first, then re-run the script with the global variable declaration and update commented out for it to actually work. I want to run the script ONCE and still get the same results.
FYI, I am a complete noob to powershell, but very familiar with the concept of variable scope.....but why is this global not working when initially created and updated, but then work the second time around, when, as far as I can tell, the CONTENT AND SCOPE of the global remains the same...…. any assistance to finding a solution to this small issue would be greatly appreciated; I have tried sooooo may different methods from inquiries through here and on Google...…..
EDIT: not sure why this will matter, because the script ran before as intended when I explicitly typed the parameter for -Path as 'C:\path\to\file'. The ONLY CHANGES MADE to the original, working script (below) were my inclusion of the global variable declaration, the update to the contents of the global variable (near the end of the function), and the attempt to use the global variable as the parameter to -Path, that is why I omitted the script:
'''
$global:af_fp = "C:\Users\timeuser\Documents\"
Function Get-Software {
[OutputType('System.Software.Inventory')]
[Cmdletbinding()]
Param(
[Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
[String[]]$Computername = $env:COMPUTERNAME
)
Begin {
}
Process {
ForEach ($Computer in $Computername) {
If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) {
$Paths = #("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SOFTWARE\\Wow6432node\\Microsoft\\Windows\\CurrentVersion\\Uninstall")
ForEach ($Path in $Paths) {
Write-Verbose "Checking Path: $Path"
# Create an instance of the Registry Object and open the HKLM base key
Try {
$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine', $Computer, 'Registry64')
}
Catch {
Write-Error $_
Continue
}
# Drill down into the Uninstall key using the OpenSubKey Method
Try {
$regkey = $reg.OpenSubKey($Path)
# Retrieve an array of string that contain all the subkey names
$subkeys = $regkey.GetSubKeyNames()
# Open each Subkey and use GetValue Method to return the required values for each
ForEach ($key in $subkeys) {
Write-Verbose "Key: $Key"
$thisKey = $Path + "\\" + $key
Try {
$thisSubKey = $reg.OpenSubKey($thisKey)
# Prevent Objects with empty DisplayName
$DisplayName = $thisSubKey.getValue("DisplayName")
If ($DisplayName -AND $DisplayName -notmatch '^Update for|rollup|^Security Update|^Service Pack|^HotFix') {
$Date = $thisSubKey.GetValue('InstallDate')
If ($Date) {
Try {
$Date = [datetime]::ParseExact($Date, 'yyyyMMdd', $Null)
}
Catch {
Write-Warning "$($Computer): $_ <$($Date)>"
$Date = $Null
}
}
# Create New Object with empty Properties
$Publisher = Try {
$thisSubKey.GetValue('Publisher').Trim()
}
Catch {
$thisSubKey.GetValue('Publisher')
}
$Version = Try {
#Some weirdness with trailing [char]0 on some strings
$thisSubKey.GetValue('DisplayVersion').TrimEnd(([char[]](32, 0)))
}
Catch {
$thisSubKey.GetValue('DisplayVersion')
}
$UninstallString = Try {
$thisSubKey.GetValue('UninstallString').Trim()
}
Catch {
$thisSubKey.GetValue('UninstallString')
}
$InstallLocation = Try {
$thisSubKey.GetValue('InstallLocation').Trim()
}
Catch {
$thisSubKey.GetValue('InstallLocation')
}
$InstallSource = Try {
$thisSubKey.GetValue('InstallSource').Trim()
}
Catch {
$thisSubKey.GetValue('InstallSource')
}
$HelpLink = Try {
$thisSubKey.GetValue('HelpLink').Trim()
}
Catch {
$thisSubKey.GetValue('HelpLink')
}
$Object = [pscustomobject]#{
#Potential Candidate for AssetID in the TIME system
AssetID = $Computer
#String that contains word or word combinations for the product field of CPE WFN; may also contain the valid values necessary for update, edition, language, sw_edition, target_hw/sw fields as well.
cpeprodinfo = $DisplayName
cpeversion = $Version
InstallDate = $Date
cpevendor = $Publisher
UninstallString = $UninstallString
InstallLocation = $InstallLocation
InstallSource = $InstallSource
HelpLink = $thisSubKey.GetValue('HelpLink')
EstimatedSizeMB = [decimal]([math]::Round(($thisSubKey.GetValue('EstimatedSize') * 1024) / 1MB, 2))
}
$Object.pstypenames.insert(0, 'System.Software.Inventory')
Write-Output $Object
}
}
Catch {
Write-Warning "$Key : $_"
}
}
}
Catch { }
$reg.Close()
}
}
Else {
Write-Error "$($Computer): unable to reach remote system!"
}
$global:af_fp = $global:af_fp + $Computer + "_AF.csv"
}
}
}
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
'''
IGNORE FORMATTING PLEASE- HAD TROUBLE MAKING INDENTS CORRECTLY FROM COPY-PASTE AND RESTRICTIONS ON SITE FOR CODE BLOCKS.....
NOTE: the ONLY changes I made, that I am asking about, are the global declaration, the global variable update in the function, and the attempt to use the global variable for the -Path parameter....script otherwise runs and will even run WITH THE LAST LINE AS IS if I ran it and errored the first time.....not sure how the addition script will help in any way, shape, or form!
With a little effort, Nasir's solution worked! HOWEVER, I ran across a sample file that had a way of adding to a parameter that inspired me to make a change to my ORIGINAL, that also worked: remove global variable from script entirely and add this code the very end:
$file_suffix = '_AF.csv'
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $env:COMPUTERNAME$file_suffix
In this way, I was able to accomplish exactly what I was setting out to do! Thanks Nasir for your response as well! I was able to also make that work as intended!
Global variables are generally frowned upon, since they often lead to poor scripts, with hard to debug issues.
It seems like your function returns some stuff, which you need to write to a file, the name of which is also generated by the same function. You can try something like this:
function function-name {
param($PathPrefix)
#do things
[pscustomobject]#{"DoThings_data" = $somevariablefromDoThings; "Filename" = "$($PathPrefix)$($variableFromDo_Things)_AF.csv"}
}
function-name -PathPrefix "C:\Path\to\folder\" | Foreach-Object { $_.DoThings_data | Export-Csv -Path $_.Filename -NoTypeInformation }
Or just have your function write the CSV data out and then return the data if you need to further process it outside the function.
Edit: this is just me extrapolating from partial code you have provided. To Lee_Dailey's point, yes, please provide more details.

Load Variables from text file and if key exists in hashtable, use key value

I'm trying to create a script, where the variables are outside of the PowerShell script. It's just a text file. In that file, we define server name, group names and their values. Group names meaning that the host has these strings in their hostname.
Hostnames have the higher priority, and Group names have the least priority. If neither is present, then there is a default value (inside the ps script).
The following elseif condition isn't working for me and I've tried a lot of other ways but could not get it working.
elseif ($MachineName.Contains("$var.key")) { # Doesn't work. What can be used here?
$foldername = ("$var.value") # Doesn't work. What can be used here?
}
Content of vars.txt:
#Groups
DB=folder7
#Hostnames
server_JIRA_001=folder3
server_DB_001=folder5
server_DB_005=folder6
If a hostname is server_DB_006 it will use value "folder7" as it has the "DB" in their hostname, but if the hostname is server_DB_005 then it will use value "folder6"
Content of script.ps1
$MachineName = "server_DB_006"
$var = (Get-Content "c:\Users\user\Desktop\vars.txt" -Raw | ConvertFrom-StringData)
if ($var.$MachineName -ne $null) {
# if hostname exists, then use its var.value
$foldername = ($var.$MachineName) # returns value of $var.hostname
} elseif ($MachineName.Contains("$var.key")) { # Doesn't work. What can be used here?
# if no hostname defined, then search for group name and use its var.value
$foldername = ("$var.value") # Doesn't work. What can be used here?
} else {
foldername = "default_folder"
}
So you can create a function who will bring back the key name if a string contains it. It loops through the keys and checks the $Text to see if it contains the key text else returns false.
function StringContainsHashTableKey(){
PARAM(
[string]$Text,
[hashtable]$HashTable
)
foreach($i in $Hashtable.Keys.GetEnumerator()){
if($Text -like "*$i*" ){
return $i
}
}
return $false
}
$Var = Get-Content "c:\Users\user\Desktop\vars.txt" -Raw | ConvertFrom-StringData
$MachineName = "server_DB_006"
$KeyMatch = StringContainsHashTableKey -Text $MachineName -HashTable $Var
if ($var.$MachineName -ne $null) { #if hostname exists, then use its var.value
$foldername = ($var.$MachineName) # returns value of $var.hostname
}
# If no hostname defined, then search for group name and use its var.value
Elseif ($KeyMatch -ne $false) { # Doesn't work. What can be used here?
$foldername = $var.$KeyMatch
}
Else {
$foldername = "default_folder"
}
$foldername

Return all SSRS reports in a folder with the data source name and ConnectString

This what I have so far. However, I want to list every report to it's connection string. I don't see a unique identifier in the GetDataSourceContents() method to join the report and data source lists.
$ReportServerUri = "YOUR_SERVER";
$rs = New-WebServiceProxy -Uri $ReportServerUri -UseDefaultCredential -Namespace "SSRS"
$rs.Url = "YOUR_SERVER"
$rs.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials;
$BICItems = $rs.ListChildren("/", $true);
$BICFolders = $BICItems | Where { $_.TypeName -eq "Folder"}
$BICDataSources = $BICItems | Where {$_.typename -eq "DataSource"}
$BICDataSourceFolders = $BICFolders | Where {$_.path -like "*Data Source*"}
$BICReports = $BICItems | Where {$_.typename -eq "Report"}
foreach ($DataSource in $BICDataSources)
{
$BICDataSourceContents = $rs.GetDataSourceContents($DataSource.Path)
$MyConnectStrings = $BICDataSourceContents | Where {$_.ConnectString -like "*MY_CONNECT_STRING*"}
$MyConnectStrings
}
I don't see a unique identifier in the GetDataSourceContents method to join the report and data source lists.
Nope. Neither do I. However when were are querying for those details we already know something unique enough. The path to the datasource itself. This is also what a report would be using so that should be a good connector.
There is a series of functions that I made to serve this purpose. Find-SSRSEntities, Get-SSRSReportDataSources and Get-SSRSDatasourceDetails are what I will try and showcase here. The last one I just made since I had no reason for those details but it was easy enough to integrate into my module.
Find-SSRSEntities
Return items from a SSRS connection. Supports loads of filtering options.
function Find-SSRSEntities{
[CmdletBinding()]
param(
[Parameter(Position=0,Mandatory=$true)]
[Alias("Proxy")]
[Web.Services.Protocols.SoapHttpClientProtocol]$ReportService,
[Parameter(Position=1)]
[Alias("Path")]
[string]$SearchPath="/",
[Parameter(Position=2)]
[ValidateSet("All", "Folder", "Report", "Resource", "LinkedReport", "DataSource", "Model")]
[Alias("Type")]
[String]$EntityType = "All",
[Parameter(Position=3)]
[String]$Match,
[Parameter(Position=4)]
[Switch]$Partial=$false
)
# Get all of the catalog items that match the criteria passed
# https://msdn.microsoft.com/en-us/library/reportservice2005.reportingservice2005.listchildren.aspx
$recursive = $true
$catalogItems = $ReportService.ListChildren($SearchPath,$recursive)
Write-Verbose "$($catalogItems.Count) item(s) located in the root path $SearchPath"
# Limit the results to the catalog types requested
if($EntityType -ne "All"){$catalogItems = $catalogItems | Where-Object{$_.Type -eq $EntityType}}
Write-Verbose "$($catalogItems.Count) item(s) found matching the type $EntityType"
# Set the match string based on parameters
if(-not $Partial.isPresent -and $Match){$Match = "^$Match$"}
Write-Verbose "Returning all items matching: '$Match'"
# If the regex is an empty string all object will be returned.
return $catalogItems | Where-Object{$_.Name -match $Match}
}
Get-SSRSReportDataSources
When given a valid report path it will return all associated datasources of that report.
function Get-SSRSReportDataSources{
[CmdletBinding()]
param(
[Parameter(Position=0,Mandatory=$true)]
[Alias("Proxy","SSRSService")]
[Web.Services.Protocols.SoapHttpClientProtocol]$ReportService,
[Parameter(Position=1,Mandatory=$true)]
[Alias("Path")]
[string]$ReportPath
)
# Test the report path to be sure it is for a valid report
if(Test-SSRSPath -ReportService $ReportService -EntityPath $ReportPath -EntityType Report){
$ReportService.GetItemDataSources($reportPath) | ForEach-Object{
[pscustomobject][ordered]#{
ReportPath = $reportPath
DataSourceName = $_.name
Reference = $_.item.reference
}
}
} else {
Write-Error "$ReportPath is not a valid report path"
}
}
Get-SSRSDatasourceDetails
When given a valid datasource path it will return all detail of that datasource. Also attaches an extra path property.
function Get-SSRSDatasourceDetails{
[CmdletBinding()]
param(
[Parameter(Position=0,Mandatory=$true)]
[Alias("Proxy")]
[Web.Services.Protocols.SoapHttpClientProtocol]$ReportService,
[Parameter(Position=1,Mandatory=$true,ValueFromPipelineByPropertyName)]
[Alias("Path")]
[string]$EntityPath
)
process{
# Split the path into its folder and entity parts
$SearchPath = Split-SSRSPath $EntityPath -Parent
$EntityName = Split-Path $EntityPath -Leaf
# Verify the path provided is to a valid datasource
if((Find-SSRSEntities -ReportService $ReportService -SearchPath $SearchPath -EntityType DataSource -Match $EntityName -Partial:$false) -as [boolean]){
Add-Member -InputObject ($ReportService.GetDataSourceContents($EntityPath)) -MemberType NoteProperty -Name "Path" -Value $EntityPath -PassThru
} else {
Write-Warning "Could not find a datasource at path: $EntityPath"
}
}
}
So armed with those lets match up all reports in a folder to their datasource connection strings. I would note that all of these functions rely on a active connection to work. Something like this
$ssrsservice = Connect-SSRSService "http://ssrsreports/ReportServer/ReportService2005.asmx" -Credential $credentials
$PSDefaultParameterValues.Add("*SSRS*:ReportService",$ssrsservice)
That will automatically apply the populated -ReportService $ssrsservice to all the SSRS functions I made below.
Else you could just add something like Find-SSRSEntities -ReportService $rs to the code below and it would work.
# Lets get all of the Marketing Datasources
$datasources = Find-SSRSEntities -SearchPath "/data sources/marketing" -EntityType DataSource | Get-SSRSDatasourceDetails
# Now gather all of their reports
Find-SSRSEntities -SearchPath "/Marketing" -EntityType Report |
# Get the report datasources
Get-SSRSReportDataSources | ForEach-Object{
# Attach the connection strings to each object
$reportDataSourceDetail = $_
# Filter the datasource for the individual datasource mapping of this report
$matchingDatasource = $datasources | Where-Object{$_.path -eq $reportDataSourceDetail.Reference}
Add-Member -InputObject $_ -MemberType NoteProperty -Name ConnectionString -Value $matchingDatasource.ConnectString -PassThru
}
This would net me results that look like this:
ReportPath : /Marketing/OandD Class Summary By Month
DataSourceName : Marketing
Reference : /Data Sources/Marketing/Marketing
ConnectionString : Data Source=SQL08R2VM; Initial Catalog=Marketing;
ReportPath : /Marketing/OandD Class YTD Summary
DataSourceName : Marketing
Reference : /Data Sources/Marketing/Marketing
ConnectionString : Data Source=SQL08R2VM; Initial Catalog=Marketing;
These, and other functions, suite me just fine. I have not really had anyone else using them so you might have issues that I have never encountered. Works fine connecting to my SSRS 2008R2 server using PowerShell v5
Here's a T-SQL statement that will return the data source name, path & connection string with the report name and path.
;WITH
XMLNAMESPACES -- XML namespace def must be the first in with clause.
(
DEFAULT 'http://schemas.microsoft.com/sqlserver/reporting/2006/03/reportdatasource'
,'http://schemas.microsoft.com/SQLServer/reporting/reportdesigner'
AS rd
)
,
shared_datasource
AS
(
SELECT
DsnSharedName = sds.[Name]
, DsnPath = sds.[Path]
, DEF = CONVERT(xml, CONVERT(varbinary(max), content))
FROM
dbo.[Catalog] AS sds
WHERE sds.[Type] = 5) --> 5 = Shared Datasource
,
data_source_name (DsnPath, DsnSharedName, DsnConnString)
AS
(
SELECT
cn.DsnPath
, cn.DsnSharedName
, cn.DsnConnString
FROM
(SELECT
sd.DsnPath
, sd.DsnSharedName
, DsnConnString = dsn.value('ConnectString[1]', 'varchar(150)')
FROM
shared_datasource AS sd
CROSS APPLY sd.DEF.nodes('/DataSourceDefinition') AS R(dsn)
) AS cn
)
SELECT
DataSourceName = lk.[Name]
, dsn.DsnPath
, dsn.DsnConnString
, ReportName = c.[Name]
, ReportFolder = c.[Path]
FROM
dbo.[Catalog] c
INNER JOIN dbo.DataSource ds ON c.ItemID = ds.ItemID
INNER JOIN dbo.[Catalog] lk ON ds.Link = lk.ItemID
INNER JOIN data_source_name dsn ON dsn.DsnSharedName = lk.[Name]
WHERE
c.[Type] = 2 --> 2 = Reports
--AND dsn.DsnConnString LIKE '%Initial Catalog%=%DatabaseNameHere%'
Then you can run the T-SQL script file in powershell with this. original post
<# Function to Check whether Server is Ping Status of the Server #>
Function Check-Ping()
{
param
(
[string]$HostName
)
$PingStatus=Get-WmiObject -Query "Select * from Win32_PingStatus where Address='$HostName'"
Return $PingStatus
}
<# Function to Check Instance name Present in the Server #>
Function Get-SQLInstances()
{
param
(
[string]$SQLServerName
)
$Status=Check-Ping($SQLServerName)
if($Status.StatusCode -ne 0)
{
Return "The Server Is Not Reachable"
}
elseif($Status.StatusCode -eq 0)
{
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $SQLServerName)
$RegKey = $Reg.OpenSubKey("SOFTWARE\\Microsoft\\Microsoft SQL Server")
$Instances=$regKey.GetValue("installedinstances")
Return $Instances
}
}
<# Function To Run TSQL and Return Results within HTML Table Tag #>
Function Run-TSQL()
{
Param
(
[string]$MachineName,
[string]$TSQLfilePath
)
$Assembly=[reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo")
$Instances=Get-SQLInstances($MachineName)
$TSQL=Get-Content $TSQLfilePath
foreach($Instance in $Instances)
{
$SQLServiceStatus=Get-Service -ComputerName $MachineName | Where-Object {$_.displayname -like "SQL Server ("+$Instance+")"}
if($SQLServiceStatus.Status -eq "Running")
{
if($Instance -eq "MSSQLSERVER")
{
$SQLServer=$MachineName
}
Else
{
$SQLServer=$MachineName+"\"+$Instance
}
$SQLServerObject = new-Object Microsoft.SqlServer.Management.Smo.Server($SQLServer)
$DatabaseObject = New-Object Microsoft.SqlServer.Management.Smo.Database
$DatabaseObject = $SQLServerObject.Databases.Item("Master")##The TSQL Script Runs in Master Database
$OutPutDataSet = $DatabaseObject.ExecuteWithResults($TSQL)
for($t=0;$t -lt $OutPutDataSet.Tables.Count;$t++)
{
$OutString+="<Table Border=2>"
$OutString+="<Tr>"
foreach($Column in $OutPutDataSet.Tables[$t].Columns)
{
$OutString+="<Th>"
$OutString+=$Column.ColumnName
$OutString+="</Th>"
}
$OutString+="</Tr>"
for($i=0;$i -lt $OutPutDataSet.Tables[$t].Rows.Count;$i++)
{
$OutString+="<Tr>"
for($j=0;$j -lt $OutPutDataSet.Tables[$t].Columns.Count;$j++)
{
$OutString+="<Td>"
$OutString+=$($OutPutDataSet.Tables[$t].Rows[$i][$j])
$OutString+="</Td>"
}
$OutString+="</Tr>"
}
$OutString+="</Table>"
$OutString+="</Br>"
$OutString+="</Br>"
}
}
}
Return $OutString
}
<# Function To Add Table Tag to with In HTML tags
Modify Title and Subject as Per yoru Requirement
#>
Function Get-HTMLOut()
{
Param
(
[String]$InputFile,
[String]$OutputFile,
[String]$TSQL
)
$Out+="<Html>"
$Out+="<Title>Run TSQL and Return HTML FIle</Title>" ## Modify 'TiTle' Tag as per your Required
$Out+="<Head><style>body {background-color:lightgray} H3{color:blue}H1{color:green}table, td, th {border: 1px solid green;}th {background-color: green;color: white;}</style></Head>" ## Modify 'Head' Tag as per your Required
$Out+="<Body><H1 Align='Center'>Run TSQL and Return HTML File</H1></Br></Br>" ## Modify 'Body' Tag as per your Required
ForEach($ServerName in Get-Content $InputFile)
{
$Out+="<H3 align='center'>--------------$ServerName--------------</H3>" ## Modify 'header Text' Tag as per your Required
$Out+="</Br>"
$Out+=Run-TSQL -MachineName $ServerName -TSQLfilePath $TSQL
}
$Out+="</Body></Html>"
Set-Content -Value $Out -Path $OutputFile
}
<# Call Get-HTMLOut Function
It Accepts 3 parameter
a. -InputFile (.txt file each server in a List withOut Instance Name)
b. -OutputFile (.Html File to which Output need to be sent)
c. -TSQL (.sql file which Contains the Script to Run)
#>
Get-HTMLOut -InputFile ".\Servers.txt" -OutputFile .\Status.Html -TSQL '.\TSQL Script.sql'

PowerShell cmdlet parameter value tab completion

How do you implement the parameter tab completion for PowerShell functions or cmdlets like Get-Service and Get-Process in PowerShell 3.0?
I realise ValidateSet works for a known list, but I want to generate the list on demand.
Adam Driscoll hints that it is possible for cmdlets but unfortunately hasn't elaborated.
Trevor Sullivan shows a technique for functions, but as I understand it, his code only generates the list at the time the function is defined.
I puzzled over this for a while, because I wanted to do the same thing. I put together something that I'm really happy with.
You can add ValidateSet attributes from a DynamicParam. Here's an example of where I've generated my ValidateSet on-the-fly from an xml file. See the "ValidateSetAttribute" in the following code:
function Foo() {
[CmdletBinding()]
Param ()
DynamicParam {
#
# The "modules" param
#
$modulesAttributeCollection = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
# [parameter(mandatory=...,
# ...
# )]
$modulesParameterAttribute = new-object System.Management.Automation.ParameterAttribute
$modulesParameterAttribute.Mandatory = $true
$modulesParameterAttribute.HelpMessage = "Enter one or more module names, separated by commas"
$modulesAttributeCollection.Add($modulesParameterAttribute)
# [ValidateSet[(...)]
$moduleNames = #()
foreach($moduleXmlInfo in Select-Xml -Path "C:\Path\to\my\xmlFile.xml" -XPath "//enlistment[#name=""wp""]/module") {
$moduleNames += $moduleXmlInfo.Node.Attributes["name"].Value
}
$modulesValidateSetAttribute = New-Object -type System.Management.Automation.ValidateSetAttribute($moduleNames)
$modulesAttributeCollection.Add($modulesValidateSetAttribute)
# Remaining boilerplate
$modulesRuntimeDefinedParam = new-object -Type System.Management.Automation.RuntimeDefinedParameter("modules", [String[]], $modulesAttributeCollection)
$paramDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add("modules", $modulesRuntimeDefinedParam)
return $paramDictionary
}
process {
# Do stuff
}
}
With that, I can type
Foo -modules M<press tab>
and it will tab-complete "MarcusModule" if that module was in the XML file. Furthermore, I can edit the XML file and the tab-completion behavior will immediately change; you don't have to re-import the function.
Check the TabExpansionPlusPlus module on github, written by a former PowerShell team magician.
https://github.com/lzybkr/TabExpansionPlusPlus#readme
Classically, I used regex.
for example,
function TabExpansion {
param($line, $lastWord)
if ( $line -match '(-(\w+))\s+([^-]*$)' )
{
### Resolve Command name & parameter name
$_param = $matches[2] + '*'
$_opt = $Matches[3].Split(" ,")[-1] + '*'
$_base = $Matches[3].Substring(0,$Matches[3].Length-$Matches[3].Split(" ,")[-1].length)
$_cmdlet = [regex]::Split($line, '[|;=]')[-1]
if ($_cmdlet -match '\{([^\{\}]*)$')
{
$_cmdlet = $matches[1]
}
if ($_cmdlet -match '\(([^()]*)$')
{
$_cmdlet = $matches[1]
}
$_cmdlet = $_cmdlet.Trim().Split()[0]
$_cmdlet = #(Get-Command -type 'Cmdlet,Alias,Function,Filter,ExternalScript' $_cmdlet)[0]
while ($_cmdlet.CommandType -eq 'alias')
{
$_cmdlet = #(Get-Command -type 'Cmdlet,Alias,Function,Filter,ExternalScript' $_cmdlet.Definition)[0]
}
### Currently target is Get-Alias & "-Name" parameter
if ( "Get-Alias" -eq $_cmdlet.Name -and "Name" -like $_param )
{
Get-Alias -Name $_opt | % { $_.Name } | sort | % { $_base + ($_ -replace '\s','` ') }
break;
}
}
}
Reference
http://gallery.technet.microsoft.com/scriptcenter/005d8bc7-5163-4a25-ad0d-25cffa90faf5
Posh-git renames TabExpansion to TabExpansionBackup in GitTabExpansion.ps1.
And posh-git's redifined TabExpansion calls original TabExpansion(TabExpansionBackup) when completions don't match with git commands.
So all you have to do is redefine TabExpansionBackup.
(cat .\GitTabExpansion.ps1 | select -last 18)
============================== GitTabExpansion.ps1 ==============================
if (Test-Path Function:\TabExpansion) {
Rename-Item Function:\TabExpansion TabExpansionBackup
}
function TabExpansion($line, $lastWord) {
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
switch -regex ($lastBlock) {
# Execute git tab completion for all git-related commands
"^$(Get-AliasPattern git) (.*)" { GitTabExpansion $lastBlock }
"^$(Get-AliasPattern tgit) (.*)" { GitTabExpansion $lastBlock }
# Fall back on existing tab expansion
default { if (Test-Path Function:\TabExpansionBackup) { TabExpansionBackup $line $lastWord } }
}
}
===============================================================================
Redefine TabExpansionBackup(original TabExpansion)
function TabExpansionBackup {
...
### Resolve Command name & parameter name
...
### Currently target is Get-Alias & "-Name" parameter
...
}

Is it possible to include functions only without executing the script?

Say I have MyScript.ps1:
[cmdletbinding()]
param (
[Parameter(Mandatory=$true)]
[string] $MyInput
)
function Show-Input {
param ([string] $Incoming)
Write-Output $Incoming
}
function Save-TheWorld {
#ToDo
}
Write-Host (Show-Input $MyInput)
Is it possible to dot source the functions only somehow? The problem is that if the script above is dot sourced, it executes the whole thing...
Is my best option to use Get-Content and parse out the functions and use Invoke-Expression...? Or is there a way to access PowerShell's parser programmatically? I see this might be possible with PSv3 using [System.Management.Automation.Language.Parser]::ParseInput but this isn't an option because it has to work on PSv2.
The reason why I'm asking is that i'm trying out the Pester PowerShell unit testing framework and the way it runs tests on functions is by dot sourcing the file with the functions in the test fixture. The test fixture looks like this:
MyScript.Tests.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
. "$here\$sut"
Describe "Show-Input" {
It "Verifies input 'Hello' is equal to output 'Hello'" {
$output = Show-Input "Hello"
$output.should.be("Hello")
}
}
Using Doug's Get-Function function you could include the functions this way:
$script = get-item .\myscript.ps1
foreach ($function in (get-function $script))
{
$startline = $function.line - 1
$endline = $startline
$successful = $false
while (! $successful)
{
try {
$partialfunction = ((get-content $script)[$startline..$endline]) -join [environment]::newline
invoke-expression $partialfunction
$successful = $true
}
catch [Exception] { $endline++ }
}
}
Edit: [System.Management.Automation.IncompleteParseException] can be used instead of [Exception] in Powershell V2.
Note -- if you find this answer helpful please upvote jonZ's answer as I wouldn't of been able to come up with this if it weren't for his helpful answer.
I created this function extractor function based on the script #jonZ linked to. This uses [System.Management.Automation.PsParser]::Tokenize to traverse all tokens in the input script and parses out functions into function info objects and returns all function info objects as an array. Each object looks like this:
Start : 99
Stop : 182
StartLine : 7
Name : Show-Input
StopLine : 10
StartColumn : 5
StopColumn : 1
Text : {function Show-Input {, param ([string] $Incoming), Write-Output $Incoming, }}
The text property is a string array and can be written to temporary file and dot sourced in or combined into a string using a newline and imported using Invoke-Expression.
Only the function text is extracted so if a line has multiple statements such as: Get-Process ; function foo () { only the part relevant to the function will be extracted.
function Get-Functions {
param (
[Parameter(Mandatory=$true)]
[System.IO.FileInfo] $File
)
try {
$content = Get-Content $File
$PSTokens = [System.Management.Automation.PsParser]::Tokenize($content, [ref] $null)
$functions = #()
#Traverse tokens.
for ($i = 0; $i -lt $PSTokens.Count; $i++) {
if($PSTokens[$i].Type -eq 'Keyword' -and $PSTokens[$i].Content -eq 'Function' ) {
$fxStart = $PSTokens[$i].Start
$fxStartLine = $PSTokens[$i].StartLine
$fxStartCol = $PSTokens[$i].StartColumn
#Skip to the function name.
while (-not ($PSTokens[$i].Type -eq 'CommandArgument')) {$i++}
$functionName = $PSTokens[$i].Content
#Skip to the start of the function body.
while (-not ($PSTokens[$i].Type -eq 'GroupStart') -and -not ($PSTokens[$i].Content -eq '{')) {$i++ }
#Skip to the closing brace.
$startCount = 1
while ($startCount -gt 0) { $i++
if ($PSTokens[$i].Type -eq 'GroupStart' -and $PSTokens[$i].Content -eq '{') {$startCount++}
if ($PSTokens[$i].Type -eq 'GroupEnd' -and $PSTokens[$i].Content -eq '}') {$startCount--}
}
$fxStop = $PSTokens[$i].Start
$fxStopLine = $PSTokens[$i].StartLine
$fxStopCol = $PSTokens[$i].StartColumn
#Extract function text. Handle 1 line functions.
$fxText = $content[($fxStartLine -1)..($fxStopLine -1)]
$origLine = $fxText[0]
$fxText[0] = $fxText[0].Substring(($fxStartCol -1), $fxText[0].Length - ($fxStartCol -1))
if ($fxText[0] -eq $fxText[-1]) {
$fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol - ($origLine.Length - $fxText[0].Length)))
} else {
$fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol))
}
$fxInfo = New-Object -TypeName PsObject -Property #{
Name = $functionName
Start = $fxStart
StartLine = $fxStartLine
StartColumn = $fxStartCol
Stop = $fxStop
StopLine = $fxStopLine
StopColumn = $fxStopCol
Text = $fxText
}
$functions += $fxInfo
}
}
return $functions
} catch {
throw "Failed in parse file '{0}'. The error was '{1}'." -f $File, $_
}
}
# Dumping to file and dot sourcing:
Get-Functions -File C:\MyScript.ps1 | Select -ExpandProperty Text | Out-File C:\fxs.ps1
. C:\fxs.ps1
Show-Input "hi"
#Or import without dumping to file:
Get-Functions -File C:\MyScript.ps1 | % {
$_.Text -join [Environment]::NewLine | Invoke-Expression
}
Show-Input "hi"