remove-variable and pipeline - null-valued expression? - powershell

I'm trying to remove some variables in my PowerShell script like so:
#create example com object
$Excel = New-Object -ComObject Excel.Application
#do something with COM object, close COM object etc
#clear variable
get-variable | where-object { $_.Value -ne $null -And $_.Value.ToString() -eq $Excel.GetType() } | remove-variable
But it returns:
You cannot call a method on a null-valued expression.
on the get-variable.... line? However, when I test that line like so:
get-variable | where-object { $_.Value -ne $null -And $_.Value.ToString() -eq $Excel.GetType() } | Select -Property Name
It returns the correct data? I get the same behaviour with:
get-variable | where-object { $_.Value -ne $null -And $_.Value.ToString() -eq $Excel.GetType() } | ForEach-Object { remove-variable -Name $_.Name }
I can't figure out why remove-variable doesn't work? The remove-variable cmdlet accepts 'Name' in the pipeline by 'PropertyName' so i think that looks ok to me?
UPDATED
Interestingly, the full error is multiple occurrences of this (interesting because the line should only return 3 results but there are 48 of these error messages):
You cannot call a method on a null-valued expression.
At line:3 char:31
+ ... re-object { $_.Value -ne $null -And $_.Value.ToString() -eq $Excel.Ge ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
ALSO
This works:
$vars = get-variable | where-object { $_.Value -ne $null -And $_.Value.ToString() -eq $Excel.GetType() } | Select-object -ExpandProperty Name
foreach ($var in $vars) {
remove-variable -Name $var
}
But this doesn't?:
get-variable | where-object { $_.Value -ne $null -And $_.Value.ToString() -eq $Excel.GetType() } | foreach { remove-variable -Name $_.Name }

I fixed it in the end by adding more null checks in the where-object part of the pipeline (thanks #ScottHeath):
get-variable | where-object { $null -ne $_ -And $null -ne $_.Value -And $null -ne $Excel -And $_.Value.ToString() -eq $Excel.GetType().ToString() } | foreach { Remove-Variable -Name $_.Name }

Com objects need to be released from memory with:
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Excel) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
After that, remove the variable itself with
$Excel = $null
Remove-Variable -Name Excel
Remove-Variable also has an -Include parameter where you can specify an array of variable names. Wildcards are also permitted there.

Related

Powershell Error handling not not working as expected with functions

Looking for advice on error handling in Powershell. I think I understand the concept behind using Try/Catch but I'm struggling on where to utilize this in my scripts or how granular I need to be.
For example, should I use the try/catch inside my functions and if so, should I insert the actions of my function inside the try or do I need to break it
down further? OR, should I try to handle the error when I call my function? Doing something like this:
Try{
Get-MyFunction
} catch{ Do Something"
}
Here's an example of a script I wrote which is checking for some indicators of compromise on a device. I have an application that will launch this script and capture the final output. The application requires the final output to be in the following format so any failure should generate this.
[output]
result=<0 or 1>
msg= <string>
Which I'm doing like this:
Write-Host "[output]"
Write-Host "result=0"
Write-Host "msg = $VariableContainingOutput -NoNewline
Two of my functions create custom objects and then combine these for the final output so I'd like to capture any errors in this same format. If one function generates an error, it should record these and continue.
If I just run the code by itself (not using function) this works but with the function my errors are not captured.
This needs to work on PowerShell 2 and up. The Add-RegMember and Get-RegValue functions called by this script are not shown.
function Get-ChangedRunKey {
[CmdletBinding()]
param()
process
{
$days = '-365'
$Run = #()
$AutoRunOutput = #()
$RunKeyValues = #("HKLM:\Software\Microsoft\Windows\CurrentVersion\Run",
"HKLM:\Software\Wow6432node\Microsoft\Windows\CurrentVersion\Run",
"HKU:\S-1-5-21-*\Software\Microsoft\Windows\CurrentVersion\Run",
"HKU:\S-1-5-21-*\Software\Wow6432node\Microsoft\Windows\CurrentVersion\Run"
)
Try{
$Run += $RunKeyValues |
ForEach-Object {
Get-Item $_ -ErrorAction SilentlyContinue |
Add-RegKeyMember -ErrorAction SilentlyContinue |
Where-Object {
$_.lastwritetime -gt (Get-Date).AddDays($days)
} |
Select-Object Name,LastWriteTime,property
}
if ($Run -ne $Null)
{
$AutoRunPath = ( $Run |
ForEach-Object {
$_.name
}
) -replace "HKEY_LOCAL_MACHINE", "HKLM:" -replace "HKEY_Users", "HKU:"
$AutoRunValue = $AutoRunPath |
Where-Object {
$_ -and $_.Trim()
} |
ForEach-Object {
Get-RegValue -path $_ -Name '*' -ErrorAction SilentlyContinue
}
}
#Build Custom Object if modified Run keys are found
if($AutorunValue -ne $null)
{
foreach ($Value in $AutoRunValue) {
$AutoRunOutput += New-Object PSObject -Property #{
Description = "Autorun"
path = $Value.path
value = $Value.value
}
}
}
Write-Output $AutoRunOutput
}catch{
$AutoRunOutput += New-Object PSObject -Property #{
Description = "Autorun"
path = "N/A"
value = "Error accessing Autorun data. $($Error[0])"
}
}
}
}
function Get-ShellIOC {
[CmdletBinding()]
param()
process
{
$ShellIOCOutput = #()
$ShellIOCPath = 'HKU:\' + '*' + '_Classes\*\shell\open\command'
Try{
$ShellIOCValue = (Get-Item $ShellIOCPath -ErrorAction SilentlyContinue |
Select-Object name,property |
ForEach-Object {
$_.name
}
) -replace "HKEY_LOCAL_MACHINE", "HKLM:" -replace "HKEY_Users", "HKU:"
$ShellIOCDetected = $ShellIOCValue |
ForEach-Object {
Get-RegValue -path $_ -Name '*' -ErrorAction SilentlyContinue
} |
Where-Object {
$_.value -like "*cmd.exe*" -or
$_.value -like "*mshta.exe*"
}
if($ShellIOCDetected -ne $null)
{
foreach ($ShellIOC in $ShellIOCDetected) {
$ShellIOCOutput += New-Object PSObject -Property #{
Description = "Shell_IOC_Detected"
path = $ShellIOC.path
value = $ShellIOC.value
}
}
}
Write-Output $ShellIOCOutput
}catch{
$ShellIOCOutput += New-Object PSObject -Property #{
Description = "Shell_IOC_Detected"
path = "N/A"
value = "Error accessing ShellIOC data. $($Error[0])"
}
}
}
}
function Set-OutputFormat {
[CmdletBinding()]
param()
process
{
$FormattedOutput = $AutoRunOutput + $ShellIOCOutput |
ForEach-Object {
"Description:" + $_.description + ',' + "Path:" + $_.path + ',' + "Value:" + $_.value + "|"
}
Write-Output $FormattedOutput
}
}
if (!(Test-Path "HKU:\")){
try{
New-PSDrive -PSProvider Registry -Root HKEY_USERS -Name HKU -ErrorAction Stop | Out-Null
}catch{
Write-Output "[output]"
Write-Output "result=0"
Write-Host "msg = Unable to Connect HKU drive" -NoNewline
}
}
$AutoRunOutput = Get-ChangedRunKey
$ShellIOCOutput = Get-ShellIOC
$FormattedOutput = Set-OutputFormat
Write-Output "[output]"
if ($FormattedOutput -eq $Null)
{
Write-Output "result=0"
Write-Host "msg= No Items Detected" -NoNewline
}
else
{
Write-Output "result=1"
Write-Host "msg=Items Detected: $($FormattedOutput)" -NoNewline
}
You have to know that there are 2 error types in PowerShell:
Terminating Errors: Those get caught automatically in the catch block
Non-Terminating Error: If you want to catch them then the command in question needs to be execution using -ErrorAction Stop. If it is not a PowerShell command but an executable, then you need to check stuff like the exit code or $?. Therefore I suggest wrapping your entire action in an advanced function on which you then call using -ErrorAction Stop.
Apart from that I would like to remark that PowerShell version 2 has already been deprecated. The reason for why non-terminating errors exists is because there are cases like for example processing multiple objects from the pipeline where you might not want it to stop just because it did not work for one object. And please do not use Write-Host, use Write-Verbose or Write-Output depending on the use case.

How to remove the error from the powershell report

i have created a powershell script to run image validation remotely and when any of the service found in stopped state this will fail the script.I am getting below error while failing the script.Is there anyway so that I can hide these error from the report?
+ Invoke-Command -ScriptBlock {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], RuntimeException
+ FullyQualifiedErrorId : ScriptHalted
This is the script i used to fail when the service is in stopped state.
$EMService = get-wmiobject win32_service | where-object {($_.Name -eq 'HP12cAgent') -or ($_.Name -eq 'HPagent12c2Agent') -or ($_.Name -eq 'HPagent10gAgent') -or ($_.Name -eq 'FarmEM10gAgent') -or ($_.Name -eq 'FarmEM11gAgent')} | format-list name | Out-String
$Servicename = $EMService.Split(":")[1].Trim()
$EMStatus1 = get-wmiobject win32_service | where-object {$_.Name -eq $Servicename} | format-list state | Out-String
$ServiceStatus = $EMStatus1.Split(":")[1].Trim()
if ($Servicename -eq $null)
{
$Servicename = "Unavailable"
}
else
{
$Servicename = "$Servicename"
}
if ($ServiceStatus -eq "Stopped")
{
throw
}
Else
{
exit 0
}
You can always try wrapping stuff in a try/catch block.
try { some powershell stuff }
catch { Write-Host $_ ; or do nothing }

How to output from a Powershell hashtable to a output file

I am trying to get the .NetFramwork version from all the windows servers. I am using powershell script. I can get the output displayed but unable to get the output from the hashtable to a output file. Also how would I get rid of the "..." from VersionDetails : {1.0.3705, 1.1.4322, 2.0.50727, 3.0...} and show the full content.
Any help will be greatly appreciated
here is the code I am using:
$username = "username"
$password = "Password"
$secstr = New-Object -TypeName System.Security.SecureString
$password.ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
$cred = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $secstr
$query = "select name from win32_directory where name like 'c:\\windows\\microsoft.net\\framework\\v%'"
$ComputerNames = Get-Content "d:\Scripts\serverList.txt"
foreach ($ComputerName in $ComputerNames)
{
write-host "ComputerName = $ComputerName"
$ComputerName | ForEach-Object {
$res = Get-WmiObject -query $query -Credential $cred -ComputerName $ComputerName | ForEach-Object {
Split-Path $_.name -Leaf } | # returns directories
Where-Object { $_ -like 'v*' } | # only include those that start with v
ForEach-Object { [system.version]( $_ -replace "^v" ) }
# remove "v" from the string and convert to version object
# Create hashtable with computername and version details
$prop = #{
ComputerName = $ComputerName
#V1_Present = &{ if ( $res | Where-Object { $_.Major -eq 1 -and $_.Minor -eq 0 } ) { $true } }
#V1_1Present = &{ if ( $res | Where-Object { $_.Major -eq 1 -and $_.Minor -eq 1 } ) { $true } }
V2_Present = &{ if ( $res | Where-Object { $_.Major -eq 2 -and $_.Minor -eq 0 } ) { $true } }
V3_Present = &{ if ( $res | Where-Object { $_.Major -eq 3 -and $_.Minor -eq 0 } ) { $true } }
V3_5Present = &{ if ( $res | Where-Object { $_.Major -eq 3 -and $_.Minor -eq 5 } ) { $true } }
V4_Present = &{ if ( $res | Where-Object { $_.Major -eq 4 -and $_.Minor -eq 0 } ) { $true } }
VersionDetails = $res
}
# Create and output PSobject using hashtable
New-Object PSObject -Property $prop
}
=========================================================
Output dispalys
PS D:\Scripts> .\GetDotNetFrameworkver.ps1
in for loop ComputerName = XXXXXXX
V4_Present : True
V3_5Present : True
V2_Present : True
V3_Present : True
ComputerName : XXXXX
VersionDetails : {1.0.3705, 1.1.4322, 2.0.50727, 3.0...}
Based on the answer of link there is a "simpler" (and faster) solution to fetch the versions.
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -recurse | Get-ItemProperty -name Version,Release -ErrorAction Ignore | Where { $_.PSChildName -match '^(?!S)\p{L}'} | Select PSChildName, Version, Release
If you want to get the versions of different remote machines you can use PowerShell remoting. Be aware that you've to enable PS remoting .If your OS version is WIN10/WIN2012R2 it is enabled per default. If you're using an older OS you've to call Enable-PSRemoting on the remote machine. See this link for details.
Example:
$result = Invoke-Command -ComputerName computer1.domain, computer1.domain -Credential (Get-Credential ) -ScriptBlock {
Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -recurse | Get-ItemProperty -name Version,Release -ErrorAction Ignore | Where { $_.PSChildName -match '^(?!S)\p{L}'} | Select PSChildName, Version, Release
}
$hash = $result | group PSComputerName -AsHashTable # Group the .Net versions by computername
$hash.'computer1.domain' # Print all .Net version of computer1
$hash.'computer1.domain'.Version # Only print the version
Hope that helps.

PS script errors in first run: The object of type ".PowerShell.Commands.Internal.Format.FormatStartData" is not valid or not in the correct sequence

I have the following function that works correctly in 2012r2, when i run it in 2008R2 it throws the below error. The surprising thing is that if i execute it a second time, it works without any issue!!
# Function Reg-Stamp {
Param(
[Parameter(Mandatory=$True)]
[string]$Phase
)
$msg = "`nEntering: $((Get-Variable MyInvocation -Scope 0).Value.MyCommand.Name)" ; Write-Host -fore Gray $msg ; $msg | out-file -Append $log
$RegStampInfo = Build-Variable $RegStampCSV
$Version = ($ScriptVersionInfo | Where-Object {$_.Parameter -eq "Version" -and $_.Phase -eq $Phase }).Value
$DisplayName = ($ScriptVersionInfo | Where-Object {$_.Parameter -eq "DisplayName" -and $_.Phase -eq $Phase }).Value
$BuildDate = ($ScriptVersionInfo | Where-Object {$_.Parameter -eq "BuildDate" -and $_.Phase -eq $Phase }).Value
$RunDate = Get-Date
$Success = $(-not($CriticalError))
$msg = "`nUpdating registry with build information"; Write-Host -fore Gray $msg; $msg | out-file $log -Append;
$RegStampInfo | Where-Object {($_.Phase.ToLower()) -eq ($Phase.ToLower())} | foreach-Object {
$ValueData = $(get-variable -Name $($_.StampData) -ValueOnly -ErrorAction SilentlyContinue)
$msg = "Adding Key: $($_.StampKey) '$($_.StampValue)' '$ValueData'"; Write-Host -fore Green "$msg"; $msg | out-file $log -Append;
New-Item -Path $($_.StampKey) -ErrorAction SilentlyContinue
Set-ItemProperty -Path $_.StampKey -name $_.StampValue -Value $ValueData
}
$msg = "`nExiting: $((Get-Variable MyInvocation -Scope 0).Value.MyCommand.Name)"; Write-Host -fore DarkGreen $msg ; $msg | out-file -Append $log
#}
I get the following error:
out-lineoutput : The object of type "Microsoft.PowerShell.Commands.Internal.For
mat.FormatStartData" is not valid or not in the correct sequence. This is likel
y caused by a user-specified "format-table" command which is conflicting with t
he default formatting.
+ CategoryInfo : InvalidData: (:) [out-lineoutput], InvalidOperat
ionException
+ FullyQualifiedErrorId : ConsoleLineOutputOutOfSequencePacket,Microsoft.P
owerShell.Commands.OutLineOutputCommand
I have seen similar error in powershell when using format-table however i am not using fr here atleast directly.
Not sure what is wrong!
EDIT:
no, but it turns out that the issue was not in the above script at all.
It appears that caller script had a line involving format-table which had nothing to do with this script, was causing the issue.
$SupportFilesInfo = Import-csv $SupportFilesCSV | select-object
$SupportFilesInfo | ft ; $SupportFilesInfo | Out-File -append $log
I changed it to:
$SupportFilesInfo | ft | out-default; $SupportFilesInfo | Out-File -append $log
which resolved the error!!
However i am still at loss at why the error occurs ONLY during the first run.
I had hit this issue earlier too, but it was very consistent.
Any idea why?

Simplify Get-Messagetrackinglog with a new function including MB Conversion

i want to create a custom function to simplify the get-messagetrackinglog commandlet.
It's nothing complicated, but simplifies the query a little bit.
The function works correctly, but i want to convert the totalbytes to Kilobyte in the function, if desired.
function Get-ExchangeMessagetrackinglog {
.Synopsys
.Description
.Example
Get-ExchangeMessagetrackinglog -Recipient "user#tld.com" -Sender "sender#tld.com" -Begin "01/04/2014" -Ende "05/05/2014" | select Timestamp,Sender,Recipients,Messagesubject,#{label="Kilobytes";Expression={[int]($_.totalbytes/1kb)} }| ft -auto
param(
[String]$ExchangeConnector = "*",
[String]$Begin=(get-date).AddDays(-120),
[Datetime]$Ende=(get-date -uformat "%m/%d/%y %T"),
[String]$Recipient = "*",
[String]$Sender = "*",
[String]$EventID = "Receive",
[String]$Source = "SMTP"
)
Get-Exchangeserver | `
where { $_.isHubTransportServer -eq $True -or $_.isMailboxServer -eq $True } | `
get-messagetrackinglog -Start $Begin -End $Ende -ResultSize Unlimited | `
where-object { `
$_.recipients -like $Recipient -and `
$_.sender -like $Sender -and `
$_.EventID -eq $EventID -and `
$_.Source -like $Source -and `
$_.connectorID -like $ExchangeConnector}
}
My Question:
Is it possible to simplify the function call (.example) ?
I'm not familiar in creating custom objects, but it is possible to create an totalkilobytes object.
Thanks!
Rather than creating a new PSCustom object my answer amends your and returns a string return ("TotalKB: " + $totalKB) as your final total. I also moved the entire select Timestamp.. block into the body of main function.
function Get-ExchangeMessagetrackinglog {
.Synopsys
.Description
.Example
Get-ExchangeMessagetrackinglog -Recipient "user#tld.com" -Sender "sender#tld.com" -Begin "01/04/2014" -Ende "05/05/2014"
param(
[String]$ExchangeConnector = "*",
[String]$Begin=(get-date).AddDays(-120),
[Datetime]$Ende=(get-date -uformat "%m/%d/%y %T"),
[String]$Recipient = "*",
[String]$Sender = "*",
[String]$EventID = "Receive",
[String]$Source = "SMTP"
)
Get-Exchangeserver | `
where { $_.isHubTransportServer -eq $True -or $_.isMailboxServer -eq $True } | `
$results = get-messagetrackinglog -Start $Begin -End $Ende -ResultSize Unlimited | `
where-object { `
$_.recipients -like $Recipient -and `
$_.sender -like $Sender -and `
$_.EventID -eq $EventID -and `
$_.Source -like $Source -and `
$_.connectorID -like $ExchangeConnector}
$totalKB = 0
foreach($entry in $results) {
$totalKB += $entry.totalbytes
}
$totalKB = $totalKB/1kb
$results | select Timestamp,Sender,Recipients,Messagesubject,#{label="Kilobytes";Expression={[int]($_.totalbytes/1kb)} }| ft -auto
return ("TotalKB: " + $totalKB)
}
Let me know how you get on as it's untested.
What do you think about this?
function Get-ExchangeMessagetrackinglog {
param(
[String]$ExchangeConnector = "*",
[String]$Begin=(get-date).AddDays(-120),
[Datetime]$Ende=(get-date -uformat "%m/%d/%y %T"),
[String]$Recipient = "*",
[String]$Sender = "*",
[String]$EventID = "Receive",
[String]$Source = "SMTP"
)
#Get-Exchangeserver | where { $_.isHubTransportServer -eq $True -or $_.isMailboxServer -eq $True } |
$Return= get-messagetrackinglog -Start $Begin -End $Ende -ResultSize Unlimited | where-object { $_.recipients -like $Recipient -and $_.sender -like $Sender -and $_.EventID -eq $EventID -and $_.Source -like $Source -and $_.connectorID -like $ExchangeConnector}
foreach ($returnvalue in $return) { $Returnvalue | add-member -MemberType Noteproperty -Name TotalKB -Value ([math]::round($returnvalue.totalbytes/ 1kb,2 ))
$Returnvalue | add-member -MemberType Noteproperty -Name TotalMB -Value ([math]::round($returnvalue.totalbytes/ 1MB,2 ))
}
$return
}
Get-ExchangeMessagetrackinglog -Begin "01/05/2014" -Ende "05/05/2014" | select timestamp,totalkb,sender,recipients,messagesubject | sort totalkb | ft -auto