Reputation: 757
I have the following Powershell script:
Function Get-InstalledPrograms {
[CmdletBinding()]
param (
[parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)][string]$computername=$env:computername,
[parameter(Mandatory=$false)] [string]$ExportFile
)
Begin {
$results = @()
function ItemizeApps ($hive, $computer, $uninstallkey) {
write-verbose "Opening registry uninstall root key $uninstallkey"
$regkey=$hive.OpenSubKey($uninstallkey)
if ($regkey) {
write-verbose "Getting uninstall subkeys"
$subkeys=$regkey.GetSubKeyNames()
write-verbose "Itemizing $($subkeys.count) subkeys"
foreach($key in $subkeys){
$path=$uninstallkey+"\"+$key
$thisSubKey=$reg.OpenSubKey($path)
write-verbose "Processing uninstall key $path"
$obj = New-Object PSObject
$obj | Add-Member -MemberType NoteProperty -Name "ComputerName" -Value $computer
$obj | Add-Member -MemberType NoteProperty -Name "UninstallKey" -Value $path
$obj | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $($thisSubKey.GetValue("DisplayName"))
$obj | Add-Member -MemberType NoteProperty -Name "DisplayVersion" -Value $($thisSubKey.GetValue("DisplayVersion"))
$obj | Add-Member -MemberType NoteProperty -Name "InstallLocation" -Value $($thisSubKey.GetValue("InstallLocation"))
$obj | Add-Member -MemberType NoteProperty -Name "Publisher" -Value $($thisSubKey.GetValue("Publisher"))
$results += $obj
}
} else {
write-verbose "No subkeys"
}
}
}
Process {
# Process HKEY_LOCAL_MACHINE uninstall keys
$reg=[microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$computername)
if ($reg) {
# Process x86 uninstalls
ItemizeApps $reg $computername "\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
# Process x86 uninstalls
ItemizeApps $reg $computername "\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
}
# Process HKEY_USERS uninstall keys
$reg=[microsoft.win32.registrykey]::OpenRemoteBaseKey('Users',$computername)
if ($reg) {
$subkeys = $reg.GetSubKeyNames()
foreach ($key in $subkeys) {
ItemizeApps $reg $computername "$key\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
}
}
}
End {
write-verbose 'Complete'
if ($ExportFile) {
$results | Where-Object { $_.DisplayName } | select ComputerName, UninstallKey, DisplayName, DisplayVersion, InstallLocation, Publisher | Export-Csv $ExportFile -append
} else {
$results
}
}
}
I am getting some weird issues with the $result += $obj. Sometimes it will add the object (first time through?) and then sometimes I get the error:
Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'. At D:\Scripts\Get-InstalledPrograms.ps1:26 char:11
+ $global:results += $obj
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (op_Addition:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
Either way, when I come out of the function, $results is empty. I am guessing it has something to do with the scoping of $results but I don't know how to fix this. I have tried $global:results and $script:results and neither work. What am I missing?
Upvotes: 0
Views: 202
Reputation: 61068
I wouldn't add objects to an array using +=
, because that is extremely inefficient as the entire array needs to be recreated on each addition.
The way your code is setup, and without making too many changes to it, I suggest you create a [System.Collections.Generic.List[object]]
to store the different results in.
You didn't tag this question PowerShell 2.0
, so I gather you use version 3.0 or better. As of PS 3.0, you can create PSObjects much easier using [PsCustomObject]@{}
.
Also I noticed you open registrykeys, but never close them..
For the revised code below I deliberately used a different name for the resulting list, because you may still have $results
as a global variable in memory.
Try
function Get-InstalledPrograms {
[CmdletBinding()]
param (
[parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[string]$computername=$env:computername,
[parameter(Mandatory=$false)]
[string]$ExportFile
)
Begin {
# create a List object to store PsObjects
$softwareList = [System.Collections.Generic.List[object]]::new()
function ItemizeApps ($hive, $computer, $uninstallkey) {
Write-Verbose "Opening registry uninstall root key $uninstallkey"
$regkey = $hive.OpenSubKey($uninstallkey)
if ($regkey) {
Write-Verbose "Getting uninstall subkeys"
$subkeys = $regkey.GetSubKeyNames()
Write-Verbose "Itemizing $($subkeys.count) subkeys"
foreach($key in $subkeys){
$path = Join-Path -Path $uninstallkey -ChildPath $key
$thisSubKey = $reg.OpenSubKey($path)
Write-Verbose "Processing uninstall key $path"
# Power shell v 3.0 has the easy [PsCustomObject] type accellerator
$softwareList.Add([PsCustomObject]@{
ComputerName = $computer
UninstallKey = $path
DisplayName = $thisSubKey.GetValue("DisplayName")
DisplayVersion = $thisSubKey.GetValue("DisplayVersion")
InstallLocation = $thisSubKey.GetValue("InstallLocation")
Publisher = $thisSubKey.GetValue("Publisher")
UninstallString = $thisSubKey.GetValue("UninstallString")
})
# close the opened key
$thisSubKey.Close()
}
# close the opened key
$regkey.Close()
} else {
Write-Verbose "No subkeys"
}
}
}
Process {
# Process HKEY_LOCAL_MACHINE uninstall keys
$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine',$computername)
if ($reg) {
# Process x86 uninstalls
ItemizeApps $reg $computername "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
# Process x86 uninstalls
ItemizeApps $reg $computername "SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
# close the opened key
$reg.Close()
}
# Process HKEY_USERS uninstall keys
$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('Users',$computername)
if ($reg) {
$subkeys = $reg.GetSubKeyNames()
foreach ($key in $subkeys) {
ItemizeApps $reg $computername "$key\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
}
# close the opened key
$reg.Close()
}
}
End {
Write-Verbose 'Complete'
if ($ExportFile) {
# since you want to export all the properties, there is no need to insert a Select-Object here
$softwareList | Where-Object { $_.DisplayName } | Export-Csv $ExportFile -Append -NoTypeInformation
} else {
$softwareList
}
}
}
Get-InstalledPrograms -Verbose
Upvotes: 1
Reputation: 25001
Theo's helpful answer gives good advice and provides a robust solution. I wanted to add explanation as to what caused errors and unintended results in your code.
When a function is called, it creates its own scope. Without targeting a specific variable scope or explicitly calling a function within a certain scope, you won't have access to its variables. If you want data created by a function, best practice will be to return that data to the calling scope and then store it.
If you call a function using .
, it will run in the current scope. Variable modifications not explicitly scoped will be reflected back in the calling scope when the function returns.
When using the global:
scope modifier, the global variable will be available in the calling and called scopes.
Using +=
to add elements to an array is not efficient. This is because you are not actually adding a new element to an array. You are outputting the current array and the new element values and storing those results into a new array. As an array gets larger, that process uses more system resources.
Regarding your issues with +=
, you will receive that error if $results
is either not already an array, is a type that can't be converted into an array with the +
operation, or a scalar that doesn't support appending with +
. When $results
is a type PSCustomObject
, you cannot add another PSCustomObject
to it to make an array.
# Example of storing function output in calling scope
$results = Get-InstalledPrograms
# Example Using . to call a function. $results will be updated in the calling scope
. Get-InstalledPrograms
$data | . Get-InstalledPrograms
# Example using global scope modifier
Function Get-InstalledPrograms {
$global:results = 'I am global'
}
Get-InstalledPrograms
$global:results
I am global
# Example of problematic +=
$results = $null
$results += [pscustomobject]@{a=1}
$results += [pscustomobject]@{a=2} # This will error because $results is not yet an array
# Example of error free +=
$results = @()
$results += [pscustomobject]@{a=1}
$results += [pscustomobject]@{a=2} # Works because $results was already an array
# Example of mixing scopes and problematic +=
$results = 25 # This is global since it is in top level scope
Function Foo {
$global:results += [pscustomobject]@{a=2}
}
Foo # Error because $results already existed as a non-array and incompatible type for the + operation in global scope
Upvotes: 1