Expand property in hashtable - arrays

I'm having a simple issue with a script, where I want to run a GCI against a remote server, issue is, the value is combined with another hashtable property, so the GCI fails.
The script reads entries from a two-column .csv, the headers are "server" and "platform"
Here's what I've got:
$ShortDate = (Get-Date).ToString('MM/dd/yyyy')
$CheckServer = #{}
$serverObjects = #() # create a list of server objects
Import-Csv $Dir\Servers.csv | ForEach {
$CheckServer.Server = $_.Server
$CheckServer.Platform = $_.Platform
if (GCI \\$_.Server\c$\log\Completed_Summary_*.html -EA 0 | where {$.LastWriteTime -ge "$ShortDate"}) {
Write-Host "FOUND"
} # end of IF GCI
} # end of For-Each
$serverObjects += New-Object -TypeName PSObject -Property $CheckServer
The problem is that the entry for $_.Server should be SERVER1, SERVER2, SERVER3, etc, all the entries in the servers.csv, instead, the values for both $_.Server and $_.Platform are combined. Such as:
Write-Host "Checking" \\#{Server=SERVER1; Platform=PLATFORM_1}.Server\c$\log\Completed_Summary_*.html
it should show as follows:
Write-Host "Checking" \\SERVER1\log\Completed_Summary_*.html
How do I un-combine them so that the GCI command works?

PowerShell only does simple variable expansion inside strings. For more complex expressions like index operations or accessing object properties/methods it would insert the stringified value of the array or object variable and leave the rest of the operation untouched.
Demonstration:
PS C:\> $array = 23, 42
PS C:\> Write-Host "some $array[1] or other"
some 23 42[1] or other
PS C:\> $object = New-Object -Type PSObject -Property #{Foo=23; Bar=42}
PS C:\> Write-Host "some $object.Foo or other"
some #{Bar=42; Foo=23}.Foo or other
To avoid this you need to either:
assign the resulting value to a variable first and use that variable in the string:
$value = $array[5]
Write-Host "some $value or other"
$value = $object.Foo
Write-Host "some $value or other"
use a subexpression ($(...)):
Write-Host "some $($array[5]) or other"
Write-Host "some $($object.Foo) or other"
use the format operator (-f):
Write-Host "some {0} or other" -f $array[5]
Write-Host "some {0} or other" -f $object.Foo

modify like it
$ShortDate = Get-Date -Hour 0 -Minute 0 -Second 0
$CheckServer = #{}
$serverObjects = #() # create a list of server objects
$Dir="C:\temp"
Import-Csv $Dir\Servers.csv | ForEach {
$CheckServer.Server = $_.Server
$CheckServer.Platform = $_.Platform
if (GCI "\\$($_.Server)\c$\log\Completed_Summary_*.html" -EA 0 | where {$_.LastWriteTime -ge $ShortDate})
{
Write-Host "FOUND"
}
}

Related

Array of variables in PowerShell has null members

I have a PowerShell script, where I want to make sure certain variables have value before proceeding.
So I have the following:
$dataRow = $sheet.Cells.Find($country).Row
$serverCol = $sheet.Cells.Find($serverString).Column
$databaseCol = $sheet.Cells.Find($databaseString).Column
$userCol = $sheet.Cells.Find($userString).Column
$passwordCol = $sheet.Cells.Find($passString).Column
$partnerCol = $sheet.Cells.Find($partnerString).Column
#All variables in this array are required. If one is empty - the script cannot continue
$requiredVars = #($dataRow, $serverCol, $databaseCol, $userCol, $passwordCol, $partnerCol)
But when I foreach over the array like so:
foreach ($var in $requiredVars)
{
Write-Host DataRow = ($dataRow -eq $var)
Write-Host ServerCol = ($serverCol -eq $var)
Write-Host DatabaseCol = ($databaseCol -eq $var)
Write-Host UserCol = ($userCol -eq $var)
Write-Host PasswordCol = ($passwordCol -eq $var)
Write-Host PartnerCol = ($partnerCol -eq $var)
if ($var -eq $null)
{
[System.Windows.Forms.MessageBox]::Show("No data found for given string!")
$excel.Quit()
return
}
}
I always get the MessageBox. I added the "Write-Host" part to see the value of each variable, then changed it to see which variable was null but all variables have values in them and all the checks you see here return "False".
I'd like to know what I'm doing wrong and if the $requiredVars array only copies values, not references or something.
Instead of using separate variables, you may consider using a Hashtable to store them all.
This makes checking the individual items a lot simpler:
# get the data from Excel and store everything in a Hashtable
# to use any of the items, use syntax like $excelData.passwordCol or $excelData['passwordCol']
$excelData = #{
'dataRow' = $sheet.Cells.Find($country).Row
'serverCol' = $sheet.Cells.Find($serverString).Column
'databaseCol' = $sheet.Cells.Find($databaseString).Column
'userCol' = $sheet.Cells.Find($userString).Column
'passwordCol' = $sheet.Cells.Find($passString).Column
'partnerCol' = $sheet.Cells.Find($partnerString).Column
}
# check all items in the hash. If any item is $null then exit
foreach ($item in $excelData.Keys) {
# or use: if ($null -eq $excelData[$item])
if (-not $excelData[$item]) {
[System.Windows.Forms.MessageBox]::Show("No data found for item $item!")
$excel.Quit()
# IMPORTANT: clean-up used COM objects from memory when done with them
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($sheet) | Out-Null
# Your code doesn't show this, but you'll have a $workbook object in there too
# [System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
return
}
}
One way to directly solve your question is this:
$a = "foo"
$b = "bar"
$c = $null
$requiredVariables = $a, $b, $c
# How many total entries in array?
($requiredVariables).Count
# How many of them have a value?
($requiredVariables | Where-Object {$_}).Count
# So one option for a single check would be:
if (($requiredVariables.Count) -ne ($requiredVariables | Where-Object {$_}).Count) {
Write-Warning "Not all values provided"
}
However an alternative [and better] approach is to make your code in to a function that includes parameter validation
function YourCustomFunction {
Param (
[ValidateNotNullOrEmpty()]
$a
,
[ValidateNotNullOrEmpty()]
$b
,
[ValidateNotNullOrEmpty()]
$c
)
Process {
Write-Output "Your function code goes here..."
}
}
# Call your function with the params
YourCustomFunction -a $a -b $b -c $c
Example output:
Test-YourCustomFunction: Cannot validate argument on parameter 'c'. The argument is null or empty. Provide an argument that is not null or empty, and
then try the command again.
At line:39 char:48

Compare values with previous loop

I made this script to monitor free space of server partitions. Now I'm trying to compare latest loop with previous one with storing free space values to arrays, but I've never done anything like this.
I don't know how exactly to work with arrays - one has to be erased at the beginning of the loop and second has to store the previous values, then they have to be compared.
Could you please give me at least a hint?
#play star-wars imperial march
function play-alarm {
Start-Job {
[Console]::Beep(440,500)
}
}
$cred = Get-Credential domain\username
$hostname = "server1"
#if free space lower, then play alarm
$low_level = "10"
#get drives letters
$partitions = Get-WmiObject Win32_LogicalDisk -Computername $hostname -Credential $cred | foreach DeviceID
#create arrays
$old_values = New-Object System.Collections.ArrayList
$new_values = New-Object System.Collections.ArrayList
#noob loop
$repeat = "2"
while ($i -lt $repeat) {
Write-Host " *** " -ForegroundColor Yellow
Write-Host " *** " -ForegroundColor Yellow
Write-Host " *** " -ForegroundColor Yellow
Write-Host "Free space at server:" -BackgroundColor Black
#backup previouse values and clear array for new ones
$old_values = $new_values
$new_values.Clear()
foreach ($partition in $partitions) {
$device = "DeviceID='" + $partition + "'"
$size = Get-WmiObject Win32_LogicalDisk -Credential $cred -ComputerName $hostname -Filter $device |
ForEach-Object {$_.Size}
$size = [Math]::Round($size/1GB)
$free = Get-WmiObject Win32_LogicalDisk -Credential $cred -ComputerName $hostname -Filter $device |
ForEach-Object {$_.FreeSpace}
$free = [Math]::Round($free/1GB)
Write-Host $free
#add value rounded to GB
$new_values.Add($free)
#if device is CD-ROM, the size or space is zero - this cause error when calculating percentage of free space
if ($size -eq "0") {
Write-Host "disk " $partition "is CD-ROM" -ForegroundColor Yellow
} else {
$perc = ($free/$size)*100
$perc = [Math]::Round($perc, 3)
if ($perc -le $low_level) {
Write-Host "Not enough free space at partition " $partition "!!!" $perc "%" -BackgroundColor Red #| play-alarm
Start-Sleep -s 15
} else {
Write-Host "disk " $partition "is OK - " $perc "%" -ForegroundColor Green
}
}
}
if ($old_values -eq $new_values) {
Write-Host "no change..."
} else {
Write-Host "Attention - change!!!" -BackgroundColor Red
}
$time = $((Get-Date).ToString())
Write-Host "Loop finished..." $time -BackgroundColor Black
Write-Host " *** " -ForegroundColor Yellow
Write-Host " *** " -ForegroundColor Yellow
Write-Host " *** " -ForegroundColor Yellow
Start-Sleep -s 300
}
Copy previous values
$old_values = $new_values
$new_values.Clear()
The problem with this is $old_values is now referring to the same array as $new_values. Use
$old_values = $new_values.Clone()
to create a copy.
Compare
You don't want to use -eq to compare the contents of container objects. You can loop through the arrays and compare each element
for ($index=0;$index -lt $old_values.count;$index+=1) {
if ($old_values[$index] -ne $new_values[$index]) {
write-host "Changed!"
break
}
}
Or you can use Compare-Object
if (Compare-Object $old_values $new_values) {
write-host "No Change!"
} else {
write-host "Changed!"
}
Compare-Object returns a list of differences. If the contents you compare are the same, then it returns an empty (and boolean false) value.
If you want to compare current values to those of a previous run you need to store the current values somewhere at the end of the script (in a CSV file for instance), and read that file (if it exists) at the beginning of the script.
$csv = 'C:\path\to\data.csv'
if (Test-Path -LiteralPath $csv) {
$old_values = Import-Csv $csv
} else {
# initialize $old_values as an empty array if the file doesn't exist (yet)
$old_values = #()
}
...
$new_values | Export-Csv $csv -NoType
Build $new_values as a list of custom objects by selecting the relevant properties of the WMI data. Avoid formatting the diskspace data unless it's for output to the user.
$new_values = Get-WmiObject Win32_LogicalDisk -Computer $hostname -Credential $cred |
Select-Object DeviceID, Size, FreeSpace
You can use calculated properties for adding derived information (like the free disk space ratio):
... | Select-Object DeviceID, Size, FreeSpace, #{n='Ratio';e={$_.FreeSpace/$_.Size}}
Use Compare-Object for comparing old and new data:
Compare-Object -ReferenceObject $old_values -DifferenceObject $new_values -Property DeviceID, Size, FreeSpace

PowerShell - Not creating Jagged Array within forEach loop

So, I'm having an issue enumerating through a forEach loop in PowerShell (v3) and adding the variable being evaluated, as well as a Test-Connection result into an array. I'm trying to make $arrPing a multi-dimensional array as this will make it easier for me to filter and process the objects in there later in the script, but I'm encountering issues with the code.
My code looks like the following:
$arrPing= #();
$strKioskIpAddress= (Get-WmiObject Win32_NetworkAdapterConfiguration | Where-Object { $_.IPAddress -ne $null }).ipaddress
...FURTHER DOWN THE CODE...
$tmpIpAddress= Select-Xml -Path $dirKioskIpAddresses -XPath '//kiosks/kiosk' | Select-Object -ExpandProperty Node
forEach ( $entry in $tmpIpAddress )
{
if ( $entry -ne $strKioskIpAddress )
{
$result= Test-Connection -ComputerName $entry -Count 1 -BufferSize 16 -Quiet -ErrorAction SilentlyContinue
$arrPing+= #($entry,$result);
}
}
But I'm getting the following output when I display the contents of the $arrPing variable:
PS H:\Documents\PowerShell Scripts> $arrPing
10.216.1.134
True
10.216.1.139
True
10.216.23.230
True
10.216.23.196
False
10.216.23.23
False
Can anyone tell me where I'm going wrong? I have a feeling that this is happening because I'm in a forEach loop but I just can't say for sure...
I would simplify it a bit by using a PSCustomObject:
$Ping = foreach ($Entry in $tmpIpAddress) {
if ($Entry -ne $strKioskIpAddress) {
$TestParams = #{
ComputerName = $Entry
Count = '1'
BufferSize = '16'
Quiet = $true
ErrorAction = 'SilentlyContinue'
}
$Result = Test-Connection #TestParams
[PSCustomObject]#{
Entry = $Entry
Result = $Result
}
}
}
$Ping
To avoid a long row of parameters I've used a technique called splatting.
You are seeing how PowerShell unrolls arrays. The variable is as designed: a large array. However PowerShell, when displaying those, puts each element on its own line. If you do not want that and especially if you are going to use This data will be used to filter out computers which are not on the network then you should use PowerShell objects.
if ( $entry -ne $strKioskIpAddress ){
$objPing += New-Object -TypeName psobject -Property #{
Entry = $entry
Result = Test-Connection -ComputerName $entry -Count 1 -BufferSize 16 -Quiet -ErrorAction SilentlyContinue
}
}
Instead of that those I would continue and use a different foreach contruct which is more pipeline friendly. That way you can use other cmdlets like Export-CSV if you need this output in other locations. Also lie PetSerAl says
[Y]ou should not use array addition operator and add elements one by one. It [will] create [a] new array (as arrays are not resizable) and copy elements from [the] old one on each operation.
$tmpIpAddress | Where-Object{$_ -ne $strKioskIpAddress} | ForEach-Object{
New-Object -TypeName psobject -Property #{
Entry = $_
Result = Test-Connection -ComputerName $_ -Count 1 -BufferSize 16 -Quiet -ErrorAction SilentlyContinue
}
} | Export-CSV -NoTypeInformation $path
The if is redundant now that we have moved that logic into Where-Object since you were using it do filter out certain records anyway. That is what Where-Object is good for.
The above code is good for PowerShell 2.0. If you have 3.0 or later then use [pscutomobject] and [ordered]
$tmpIpAddress | Where-Object{$_ -ne $strKioskIpAddress} | ForEach-Object{
[psobject][ordered] #{
Entry = $_
Result = Test-Connection -ComputerName $_ -Count 1 -BufferSize 16 -Quiet -ErrorAction SilentlyContinue
}
} | Export-CSV -NoTypeInformation $path

Powershell - combining arrays

I am new to powershell and in need of help. My first script for work is automate the new and termed users in AD environment.
A CSV dump will be done once daily from our Peoplesoft system. I use Import-CSV and create 3 arrays (new, term and processed).
The trouble I'm having is with combining the 3 arrays once i loop through all the users and try putting it back into the file. The code breaks at the $New += $Term lines. I believe this is due to the fact that there is only 1 record of each user type (new, term and processed) in my test file (I know, add more users…can't. This may be a real world outcome for any particular day). Below is my sample code:
#Get Credentials from user
$c = Get-Credential
#Get Date for $Term array population
$e = Get-Date -format M/d/yyyy
#Set file location and variable for said file
$File = "c:\users\nmaddux\desktop\adduserstuff\test.csv"
#Import record sets for New and Term users
$New = #()
$Term = #()
$Procd = #()
$New = Import-Csv $File | Where-Object {
$_.TermDate -eq "" -and $_.LastName -ne "" -and $_.Processdate -eq ""
}
$Term = Import-Csv $File | Where-Object {
$_.TermDate -ne "" -and $_.Processdate -eq "" -and $_.TermDate -le $e
}
$Procd = Import-Csv $File | Where-Object { $_.Processdate -ne "" }
#Process both new and term users provided there are records to process for each
If ($New -ne $NULL -and $Term -ne $NULL) {
# Some code to process users
}
$new += $term
$new += $Procd
$new | Export-Csv $file -NoTypeInformation -ErrorAction SilentlyContinue
So it will export but only partial results.
error - Method invocation failed because [System.Management.Automation.PSObject] doesn't contain a method named 'op_Addition'.
If Import-Csv only returns 1 result, then you are correct that your variable is assumed NOT to be an array, then concatenation will fail. This is not change by the fact that you have pre-initialized your variables with #(). In fact, that step isn't necessary.
To force the result to be treated as an array, you can either wrap your whole Import-Csv line in #(), or do something similar afterward.
$new = #( Import-Csv $File | Where-Object {...} )
# or
$new = Import-Csv $File | Where-Object {...}
$new = #($new)
So you are importing the same CSV file 3 times? isn't it better to import it once and then set the arrays to be filtered "views" of it?
Sort of like this. You should also be able to use the "Count" value from each array as well to say whether 1 or more results were returned.
#Get Credentials from user
$c = Get-Credential
#Get Date for $Term array population
$e = Get-Date -format M/d/yyyy
#Set file location and variable for said file
$File = "c:\users\nmaddux\desktop\adduserstuff\test.csv"
#Import record sets for New and Term users
[array]$New
[array]$Term
[array]$Procd
[array]$Import = Import-Csv $File
[array]$New = $Import | ? {$_.TermDate -eq "" -and $_.LastName -ne "" -and $_.Processdate -eq ""}
[array]$Term = $Import | ? {$_.TermDate -ne "" -and $_.Processdate -eq "" -and $_.TermDate -le $e}
[array]$Procd = $Import | ? {$_.Processdate -ne ""}
#Process both new and term users provided there are records to process for each
if (($New.Count -gt 0) -and ($Term.Count -gt 0))
{
# Some code to process users
}
$new += $term
$new += $Procd
$new | Export-Csv $file -NoTypeInformation -ErrorAction SilentlyContinue
You can also enforce the type by typecasting the variable:
$array = #()
$array = gci test.txt
$array.GetType()
[array]$array = #()
$array = gci test.txt
$array.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True FileInfo System.IO.FileSystemInfo
True True Object[] System.Array
I know I'm coming into this discussion late, but for someone else that comes along...
Since you already defined $new as an empty array, when you import from the csv you want to ADD the output to your pre-defined array, not set it equal to the output of import-csv.
$new = #()
$new += Import-Csv $File | Where-Object {
$_.TermDate -eq "" -and $_.LastName -ne "" -and $_.Processdate -eq ""
}

PowerShell: Set-Content having issues with "file already in use"

I'm working on a PowerShell script that finds all the files with PATTERN within a given DIRECTORY, prints out the relevant lines of the document with the PATTERN highlighted, and then replaces the PATTERN with a provided REPLACE word, then saves the file back. So it actually edits the file.
Except I can't get it to alter the file, because Windows complains about the file already being open. I tried several methods to solve this, but keep running into the issue. Perhaps someone can help:
param(
[string] $pattern = ""
,[string] $replace = ""
,[string] $directory ="."
,[switch] $recurse = $false
,[switch] $caseSensitive = $false)
if($pattern -eq $null -or $pattern -eq "")
{
Write-Error "Please provide a search pattern." ; return
}
if($directory -eq $null -or $directory -eq "")
{
Write-Error "Please provide a directory." ; return
}
if($replace -eq $null -or $replace -eq "")
{
Write-Error "Please provide a string to replace." ; return
}
$regexPattern = $pattern
if($caseSensitive -eq $false) { $regexPattern = "(?i)$regexPattern" }
$regex = New-Object System.Text.RegularExpressions.Regex $regexPattern
function Write-HostAndHighlightPattern([string] $inputText)
{
$index = 0
$length = $inputText.Length
while($index -lt $length)
{
$match = $regex.Match($inputText, $index)
if($match.Success -and $match.Length -gt 0)
{
Write-Host $inputText.SubString($index, $match.Index) -nonewline
Write-Host $match.Value.ToString() -ForegroundColor Red -nonewline
$index = $match.Index + $match.Length
}
else
{
Write-Host $inputText.SubString($index) -nonewline
$index = $inputText.Length
}
}
}
Get-ChildItem $directory -recurse:$recurse |
Select-String -caseSensitive:$caseSensitive -pattern:$pattern |
foreach {
$file = ($directory + $_.FileName)
Write-Host "$($_.FileName)($($_.LineNumber)): " -nonewline
Write-HostAndHighlightPattern $_.Line
%{ Set-Content $file ((Get-Content $file) -replace ([Regex]::Escape("[$pattern]")),"[$replace]")}
Write-Host "`n"
Write-Host "Processed: $($file)"
}
The issue is located within the final block of code, right at the Get-ChildItem call. Of course, some of the code in that block is now a bit mangled due to me trying to fix the problem then stopping, but keep in mind the intent of that part of the script. I want to get the content, replace the words, then save the altered text back to the file I got it from.
Any help at all would be greatly appreciated.
Removed my previous answer, replacing it with this:
Get-ChildItem $directory -recurse:$recurse
foreach {
$file = ($directory + $_.FileName)
(Get-Content $file) | Foreach-object {
$_ -replace ([Regex]::Escape("[$pattern]")),"[$replace]")
} | Set-Content $file
}
Note:
The parentheses around Get-Content to ensure the file is slurped in one go (and therefore closed).
The piping to subsequent commands rather than inlining.
Some of your commands have been removed to ensure it's a simple test.
Just a suggestion but you might try looking at the documentation for the parameters code block. There is a more efficient way to ensure that a parameter is entered if you require it and to throw an error message if the user doesn't.
About_throw: http://technet.microsoft.com/en-us/library/dd819510.aspx
About_functions_advanced_parameters: http://technet.microsoft.com/en-us/library/dd347600.aspx
And then about using Write-Host all the time: http://powershell.com/cs/blogs/donjones/archive/2012/04/06/2012-scripting-games-commentary-stop-using-write-host.aspx
Alright, I finally sat down and just typed everything sequentially in PowerShell, then used that to make my script.
It was actually really simple;
$items = Get-ChildItem $directory -recurse:$recurse
$items |
foreach {
$file = $_.FullName
$content = get-content $file
$newContent = $content -replace $pattern, $replace
Set-Content $file $newcontent
}
Thanks for all your help guys.

Resources