Counting in powershell? - powershell

Can someone explain to me whats going on here? Its a piece of code i got from a script we use here at work and i believe that i understand why it counts but, from there im lost. Any generalization on why/how it does so would be greatly appreciated.
Please note, i did search everywhere before asking on here.
$gc = Get-ChildItem C:\users | Select-Object -ExpandProperty Name
$ls = #($gc)
$gcls = $ls.count
For($i=0; $I -lt $gcls; $i++){
Write-host "$($i): $($ls[$i])"
}
$selection = Read-Host "Enter Number"
$selection = $selection -split " "
$gc[$selection]
gc is self explanatory.
ls is as well throwing the output into an array
gcls is creating the variable to the list of counted strings
I kinda understand whats going on in the for statement where its setting $i to 0, saying if $i -lt the counted strings in $gcls (which it is due to $i=0), and it is counting the output. Now im still kind of following but, I just don't seem to understand how its outputting the strings the way it is.
Anyone familiar with this?

Lee_Dailey also answered this above as a comment.
Inlined comments explaining what each line does and where the count comes from, how the write-host works, etc.
$gc = Get-ChildItem C:\users | Select-Object -ExpandProperty Name #gets all items in c:\users
$ls = #($gc) #this seems redundant to me, but, puts output from get-childitem above into $ls
$gcls = $ls.count #stores a count of items found in get-childitem in $gcls
For($i=0; $I -lt $gcls; $i++){
<#
check out https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_for?view=powershell-7.1
specifically:
The For statement (also known as a For loop) is a language construct you can use to create a loop
that runs commands in a command block while a specified condition evaluates to $true.
So this will run the statement in the scriptblock (Write-host) while $i is less than $gcls (the count of items found in get-childitem).
each time it loops, it willll print $($i): $($ls[$i]) to the console and then increase $i by 1 (the $i++ in the For)
breaking down the print statement:
$($i) - prints the current loop count. The $() is a subexpression operator. It isnt really needed here, but it isnt hurting anything see https://ss64.com/ps/syntax-operators.html
$($ls[$i]) - we have a subexpression operator again. This time were printing a value in the variable $ls. The [$i] gets an item from the array. We need the $(), otherwise it would print all the contents of $ls rather than just the one item we wanted - try it yourself write-host "$($ls[0])" vs write-host "$ls[0]"
$ls[0] would get the first item in the array
$ls[1] would get the second, so on and so forth. Can see https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_arrays?view=powershell-7.1 for more info
#>
Write-host "$($i): $($ls[$i])"
}
$selection = Read-Host "Enter Number" #prompts the user for input, expects INT seperated by spaces - 5 10
$selection = $selection -split " " #splits the user input
$gc[$selection] #prints the names using user input above. If the user enters 5, theyd get the 5th item returned by Get-ChildItem. Entering "5 10" would get the 5th and the 10th item. Again, see about_arrays above

Related

Powershell script variable treats comparison operations differently

I have an odd issue where a variable that is part of an if statement is not performing a comparison to a value as expected
The value is in a CSV file. It is a number, not a string so it should work fine. If the value comparison is set to -eq "0" it will print out the file entries that have only zero. If I use -gt it lists only the values entries not zero. So far so good.
If I use -le or -lt all file entries will get printed. If I use multiple comparisons such as -gt "0" and -lt "5" this also does not work - it prints out all values (except those equal to zero of course). I'm at a loss how to get the code to work properly (ignore Write-host, that is just so I can see the output).
I'm using Foreach because I'll never have more than about 10-20 entries, otherwise I'd go with Foreach-Object. Code details are below:
$extractFile = "C:\SrvInventory\DBLoginChk.csv"
$passwdexp = 5
$DBdetails = Import-csv $extractfile
Foreach ($DBCreds in $DBdetails) {
If (($DBCreds.DaysUntilExpiration -gt "0") -and
($DBCreds.DaysUntilExpiration -le $passwdexp)) {
$Expdt = $($DBCreds.DaysUntilExpiration)
$DBName = $($DBCreds.LoginName)
$Emailaddr = $DBName+'#'+'myplace.com'
Write-host $Expdt
}
}
File contents look like this:
"LoginName","DaysUntilExpiration"
"spot","35"
"pup","22"
"cat","21"
"rhino","3"
"camel","1"
"Bull","0"
"warthog","0"
"GViewer","0"
"grandrapids","0"

Issues with Count as an element in array conflicting with count. Powershell

I'm currently writing a PoSH script to snapshot our VMware servers. Running into a hitch as I'd like for the script to remove the oldest snapshot when more than 3 are taken.
I have put the servers I want to snap and the count of current snapshots into an Array, however when I use the logic of
IF ($variable | where-object {$_.count -gt 3}) {
write-host $_.name }
The script returns nothing for the script block, when I run it manually $variable.count returns the count not the value of the element in the array (I guess because of the membertype).
I.e. if I have two items in the array it returns a value of 2 instead of the number in the count column i.e. 5.
I'm probably doing something very stupid but the PowerCLI object for snapshots uses count as the element name.
Any ideas gratefully received.
Thanks
The example you pasted taken literally means: "Evaluate elements in $variable that have a count greater than three. If this evaluates to true, then write the name property of the $_ (current pipeline object) to host." Which doesn't seem quite what you wanted, because it:
asserts that each element in your $variable is itself a collection, by checking for counts on the current pipeline object and not the collection as a whole.
causes the if statement to never evaluate to true because your elements aren't collections, and thus don't have counts to be greater than anything.
uses the current pipeline object variable $_ outside of a pipeline.
This simple example is probably closer to what you want. For the sake of example, lets remove properties and just use a simple string array. It will print the name of the last item in a collection if the collection's size is greater than 3:
PS C:> $arr = "test1","test2","test3","test4"
PS C:> if($arr.Count -gt 3) { Write-Host $arr[-1] }
test4
Feel free to build on this; I'm not sure from your question body of some more specific details about which array element specifically you want to remove. For example, if you want to remove the first element instead of the last, you'll want to use $arr[0] to select that element.
You can use the below code for your reference:
$maxtasks = 4
$snaps = get-vm | get-snapshot -name "before power down"
$i = 0
while($i -lt $snaps.Count){
Remove-Snapshot -Snapshot $snaps[$i] -RunAsync -Confirm:$false
$tasks = Get-Task -Status "Running" | where {$_.Name -eq "RemoveSnapshot_Task"}
while($tasks.Count -gt ($maxtasks-1)) {
sleep 30
$tasks = Get-Task -Status "Running" | where {$_.Name -eq "RemoveSnapshot_Task"}
}
$i++
}
As long as there are more than 3 - then the script will sleep, once there are less than 3 it will increment $i by 1 and will continue to remove the next snapshot.
Refer the link for your reference. Throttle Snapshot
And if you want to remove based on number of days, then use this approach:
Here the span is of 30 days.
$oneMonthAgo = (Get-Date).AddDays(-30)
Get-VM | Foreach-Object {
Get-Snapshot -VM $_ | Foreach-Object {
if($_.Created -lt $oneMonthAgo) {
Remove-Snapshot $_ -Confirm -WhatIf
}}}
Note: Be very careful on Remove-Snapshot cause it could potentially do a lot of damage
Hope this helps you.

Powershell - how can I make

Trying to find the numbers in my file divisible by 3. How can I make my for each loop read each number individually?
this is my file:
6 9 7
-----
5 2 9
3 4 4
1 6 9
This is my code so far:
function number{
param($b)
# Loop through all lines of input
foreach($a in $b){
if ($line = % 3)
{
Write-Output "divisible by 3"
}
else {
Write-Output "number not divisible by 3"
}
}
}
#Variables
#Get input from csv file
$a = Import-Csv "document 2.Dat"
How have you got this far in, without realising that none of that even does anything at all? Whatever development approach you're using, you need to rethink it.
Hints that something is wrong:
How is it printing more dashes than there even are in the file? What is it actually printing? Use useful debugging/testing tool, wrap each thing in dashes so you can see where they start and end:
Oh that's broken.
Inside function number { put write-host 'hello?' and see that it's never printing anything.
Try calling the function by hand to see what it does:
Oh I have no idea what number is not divisible by 3, I'd better fix that so I can see what's going on.
And if you have an eye looking for details
where does $line get assigned? What is = doing in an if test? What is % 3 doing with nothing to the left of the %? Why am I using variable names like $a and $b which don't help me follow what's happening at all?
and, of course, "*why am I not write-host "..." all the way through, and/or stepping through this code in the debugger to see what's happening?
Google(*) "read file powershell"
Try it
That's my file alright. And the limits of the output are ... lines. Cool.
function number I should give it a better name but.
Sigh. alright, alright.
No output, even from a simple 'hi'? Ah, call the function.
Great.
Pass a parameter to it and print it...
No output.
Enough screenshots.
Pass a parameter when calling the function. Get-NumbersWhichDivideEvenlyByThree $FileContent
Iterate over the lines and print them inside the function.
Google "powershell get numbers from string" and stuff
Iteratively develop your code, going from working block to working block. Never end up in a position where you have a dozen lines that all don't work in half a dozen different ways all at once, and nowhere to go from there.
Bit you actually asked
Get numbers out of a string.
Use regex. This is exactly why they exist. But to try and keep it simple - in a way that's actually more complicated but tough - break the lines apart on spaces, and pick out the pieces which are numbers and throw the rest away.
To get this with a reasonably nice answer, you almost need to just magically know about -split, perhaps by stumbling on one of #mklement0's answers here about unary split or split has an unary form or the unary form of the -split operator is key here , or, I guess, have read help about_Split in careful detail.
-split '6 9 7' # this splits the line into parts on *runs* of whitespace
6
9
7 # look like numbers, but are strings really
So you get some text pieces, including the line of ----- in the file, that will be among them. And you need to test which are numbers and keep them, and which are dashes (letters, punctuation, etc) and throw those away.
$thing -as [int] # will try to cast $thing as a (whole) number, and silently fail (no exception) if it cannot.
# Split the line into pieces. Try to convert each piece to a number.
# Filter out the ones which weren't numbers and failed to convert.
$pieces = -split $line
$pieces = $pieces | ForEach-Object { $_ -as [int] }
$numbers = $pieces | Where-Object { $_ -ne $null }
Then you can do the % 3 test. And have code like:
function Get-NumbersWhichDivideEvenlyByThree {
param($lines)
foreach ($line in $lines)
{
$pieces = -split $line
$pieces = $pieces | ForEach-Object { $_ -as [int] }
$numbers = $pieces | Where-Object { $_ -ne $null }
foreach ($number in $numbers)
{
if (0 -eq $number % 3)
{
Write-Output "$number divisible by 3"
}
else
{
Write-Output "$number not divisible by 3"
}
}
}
}
$FileContent = Get-Content 'D:\document 2.dat'
Get-NumbersWhichDivideEvenlyByThree $FileContent
and output like:
(-split(gc D:\test.txt -Raw)-match'\d+')|%{"$_$(('',' not')[$_%3]) divisible by 3"}

Powershell assistance

I am currently using the below PS script to check if the currents months MS patches are installed on the system. The script is set to check the $env:COMPUTERNAME.mbsa and the Patch_NA.txt file and send the result to the $env:COMPUTERNAME.csv file.
I now need to modify this script to also pull information from other POS devices in the same location (C:\Users\Cambridge\SecurityScans) and send the results to the $env:COMPUTERNAME.csv file.
The POS devices are listed like this:
172.26.210.1.mbsa
172.26.210.2.mbsa
172.26.210.3.mbsa
and so forth.
The IP range at all our locations (last octet) is 1 - 60. Any ideas on how I can set this up?
Script:
$logname = "C:\temp\PatchVerify\$env:COMPUTERNAME.csv"
[xml]$x=type "C:\Users\Cambridge\SecurityScans\$env:COMPUTERNAME.mbsa"
#This list is created based on a text file that is provided.
$montlyPatches = type "C:\Temp\PatchVerify\Patches_NA.txt"|
foreach{if ($_ -mat"-KB(? <KB>\d+)"){$matches.KB}}
$patchesNotInstalled=$x.SecScan.check | where {$_.id -eq 500} |foreach{`
$_.detail.updatedata|where {$_.isinstalled -eq "false"}}|Select -expandProperty KBID
$patchesInstalled =$x.SecScan.check | where {$_.id -eq 500} |foreach{`
$_.detail.updatedata|where {$_.isinstalled -eq "true"}}|Select -expandProperty KBID
"Store,Patch,Present"> $logname
$store = "$env:COMPUTERNAME"
foreach ($patch in $montlyPatches)
{
$result = "Unknown"
if ( $patchesInstalled -contains $patch)
{
$result = "YES"
}
if ( $patchesNotInstalled -contains $patch)
{
$result = "NO"
}
"$store,KB$($patch),$result" >>$logname
}
You can find lots of information on creating functions on the web, but a simple example would be:
Function Check-Patches{
Param($FileName)
$logname = "C:\temp\PatchVerify\$FileName.csv"
[xml]$x=type "C:\Users\Cambridge\SecurityScans\$FileName.mbsa"
The rest of your existing code goes here...
}
Check-Patches "$env:ComputerName"
For($i=1;$i -le 60;$i++){
Check-Patches "172.26.210.$i"
}
If you need me to break down anything in that let me know and I'll go into further explanation, but from what you already have it looks like you have a decent grasp on PowerShell theory and just needed to know what resources are available.
Edit: I updated my example to better fit your script, having it accept a file name, and then applying that file name to the $logname and $x variables within the function.
The break down...
First we declare that we are creating a Function using the Function keyword. Following that is the name of the function that you will use later to call it, and an opening curly brace to start the scriptblock that makes up the actual function.
Next is the Param line, which in this case is very simple only declaring one variable as input. This could alternatively be done as Function Check-Patches ($FileName){ but when you start getting into more advanced functions that only gets confusing, so my recommendation is to stick with putting the parameters inside the function's scriptblock. This is the first thing you want inside of your function in most cases, excluding any Help that you would write up for the function.
Then we have updated lines for $logname and [xml]$x that use the $FileName that the function gets as input.
After that comes all of your code that parses the patch logs, and outputs to your CSV, and the closing curly brace that ends the scriptblock, and the function.
Then we call it for the ComputerName, and run a For loop. The For loop runs everything between 1 and 60, and for each loop it uses that number as the last octet of the file name to feed into the function and check those files.
A few comments on the rest of your code. $monthlypatches = could be changed to = type | ?{$_ -match "-KB(? <KB>\d+)"}|%{$matches.KB} so that the results are filtered before the ForEach loop, which could cut down on some time.
On the $patchesInstalled and $patchesNotInstalled lines you don't need the backtick at the end of that line. You can naturally have a linebreak after the beginning of the scriptblock for a ForEach loop. Having it there can be hard to see later if the script breaks, and if there is anything after it (including a space) the script can break and throw errors that are hard to track down.
Lastly, you loop through $x twice, and then $monthlyPatches once, and do a lot of individual writes to the log file. I would suggest creating an array, filling it with custom objects that have 3 properties (Store, Patch, and Present), and then outputting that at the end of the function. That changes things a little bit, but then your function outputs an object, which you could pipe to Export-CSV, or maybe later you could want it to do something else, but at least then you'd have it. To do that I'd run $x through a switch to see if things are installed, then I'd flush out the array by setting all of the monthlypatches that aren't already in that array to Unknown. That would go something like:
Function Check-Patches{
Param($FileName)
$logname = "C:\temp\PatchVerify\$FileName.csv"
[xml]$x=type "C:\Users\Cambridge\SecurityScans\$FileName.mbsa"
$PatchStatus = #()
#This list is created based on a text file that is provided.
$monthlyPatches = GC "C:\Temp\PatchVerify\Patches_NA.txt"|?{$_ -match "-KB(? <KB>\d+)"} | %{$matches.KB}
#Create objects for all the patches in the updatelog that were in the monthly list.
Switch($x.SecScan.Check|?{$_.KBID -in $monthlyPatches -and $_.id -eq 500}){
{$_.detail.updatedata.isinstalled -eq "true"}{$PatchStatus+=[PSCustomObject][Ordered]#{Store=$FileName;Patch=$_.KBID;Present="YES"};Continue}
{$_.detail.updatedata.isinstalled -eq "false"}{$PatchStatus+=[PSCustomObject][Ordered]#{Store=$FileName;Patch=$_.KBID;Present="NO"};Continue}
}
#Populate all of the monthly patches that weren't found on the machine as installed or failed
$monthlyPatches | ?{$_ -notin $PatchStatus.Patch} | %{$PatchStatus += [PSCustomObject][Ordered]#{Store=$FileName;Patch=$_;Present="Unknown"}}
#Output results
$PatchStatus
}
#Check patches on current computer
Check-Patches "$env:ComputerName"|Export-Csv "C:\temp\PatchVerify\$env:ComputerName.csv" -NoTypeInformation
#Check patches on POS Devices
For($i=1;$i -le 60;$i++){
Check-Patches "172.26.210.$i"|Export-Csv "C:\temp\PatchVerify\172.26.210.$i.csv" -NoTypeInformation
}

Powershell being too clever

Apologies for what is probably a newbish question.
I am writing some Powershell scripts that run various queries against AD. They will usually return a bunch of results, which are easy to deal with, ie:
$results = myQuery
write-host "Returned " + $results.Count + " results."
foreach ($result in $results) { doSomething }
No worries. However, if there is only 1 result, Powershell automagically translates that result into a single object, rather than an array that contains 1 object. So the above code would break, both at the Count and the foreach. I'm sure the same problem would occur with 0 results.
Could someone suggest an elegant way to handle this? Perhaps some way to cast the results so they are always an array?
Change the first line of code to
$results = #(myQuery)
This will always return an array. See this blog entry for additional details.
Actually, the foreach works just fine. All uses of foreach (the foreach keyword, the Foreach-Object cmdlet, and Foreach-Object's aliases "foreach" and "%") all have the same behavior of "wrapping" the object in question in an array if needed. Thus, all uses of foreach will work with both scalar and array values.
Annoyingly, this means they work with null values too. Say I do:
$x = $null
foreach ($y in $x) {Write-Host "Hello world 1"}
$x | Foreach-Object {Write-Host "Hello world 2"}
I'll get
"Hello world 1"
"Hello world 2"
out of that.
This has bitten me as well. No clever ideas on how to fix $results.Count, but the foreach can be fixed by switching to a pipeline.
$scalar = 1
$list = (1,2)
$list | % { $_ }
prints
1
2
$scalar | % { $_ }
prints
1