Passing variables to runspaces and back - powershell

so I have the following code:
#region Initialize stuff
$files = gci "C:\logs\*.log"
$result = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$RunspaceCollection = #()
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$ScriptBlock = {
Param($file, $result)
$content = Get-Content $file.FullName -ReadCount 0
foreach ($line in $content) {
if ($line -match 'A002') {
[void]$result.Add($($line -replace ' +',","))
}}}
#endregion
#region Process Data
Foreach ($file in $files) {
$Powershell = [PowerShell]::Create().AddScript($ScriptBlock).AddArgument($file).AddArgument($result)
$Powershell.RunspacePool = $RunspacePool
[Collections.Arraylist]$RunspaceCollection += New-Object -TypeName PSObject -Property #{
Runspace = $PowerShell.BeginInvoke()
PowerShell = $PowerShell
}}
While($RunspaceCollection) {
Foreach ($Runspace in $RunspaceCollection.ToArray()) {
If ($Runspace.Runspace.IsCompleted) {
[void]$result.Add($Runspace.PowerShell.EndInvoke($Runspace.Runspace))
$Runspace.PowerShell.Dispose()
$RunspaceCollection.Remove($Runspace)
}}}
#endregion
#region Parse Data
$data = ConvertFrom-Csv -InputObject $result -Header "1","2","3","TimeIn","TimeOut","4","5","Dur"
foreach ($line in $data) {
if ($line.TimeIn -match "A002") { $TimeIn += [timespan]::Parse($line.Dur) }
else { $TimeOut += [timespan]::Parse($line.Dur) }}
#endregion
It works, but I don't completely understand how ;)
what is [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) and why does it work, while regular ArrayList doesn't? Why do I need this "Synchronized" and what it does? Could you please explain or point me to some materials about this? I can't seem to find anything relevant. Thank you!

To guarantee the thread safety of the ArrayList, all operations must
be done through this wrapper.
Enumerating through a collection is intrinsically not a thread-safe
procedure. Even when a collection is synchronized, other threads can
still modify the collection, which causes the enumerator to throw an
exception. To guarantee thread safety during enumeration, you can
either lock the collection during the entire enumeration or catch the
exceptions resulting from changes made by other threads.
Source:
https://msdn.microsoft.com/en-us/library/3azh197k(v=vs.110).aspx
This was found by googling
ArrayList Synchronized Method
It would be most beneficial for you to learn the fundamentals of object oriented script/programming so in the future you know exactly what to look for. There are many ways to implement .NET namespaces, classes, methods, and elements in a powershell host, [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) being one of them.

Related

Looking for docs/explainer on powershell syntax ":Label foreach ($item in $items) { }"

So hard to Google this one...
Looking for docs/explainer on the syntax :Label foreach ($item in $items) { }
I came across an interesting example in the official docs and I'm trying to wrap my head around some of the concepts used. The example I'm referencing is at the very bottom of the about_foreach page (using Get-Help) and also online here: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_foreach?view=powershell-7.3#:~:text=%3AtokenLoop%20foreach
The example defines an AST/parser utility for showing info about where functions are defined within a given script file (pretty cool advanced example imo). There are a few concepts in the example that I've seen before and understand the usefulness of, but haven't used personally, like do/until statements and enumerator methods like $foreach.MoveNext()
But it's the first time I've seen the :myLabel for () {} syntax which seems to only be relevant to specific expressions like loops, and I'm curious about the usage of this construct (like how/can you reference this label), does anyone here make use of this or know where to find docs/explainer on it?
Thanks in advance !
Here's the full raw example from the docs in case you like clicking links:
function Get-FunctionPosition {
[CmdletBinding()]
[OutputType('FunctionPosition')]
param(
[Parameter(Position = 0, Mandatory,
ValueFromPipeline, ValueFromPipelineByPropertyName)]
[ValidateNotNullOrEmpty()]
[Alias('PSPath')]
[System.String[]]
$Path
)
process {
try {
$filesToProcess = if ($_ -is [System.IO.FileSystemInfo]) {
$_
} else {
Get-Item -Path $Path
}
$parser = [System.Management.Automation.Language.Parser]
foreach ($item in $filesToProcess) {
if ($item.PSIsContainer -or
$item.Extension -notin #('.ps1', '.psm1')) {
continue
}
$tokens = $errors = $null
$ast = $parser::ParseFile($item.FullName, ([REF]$tokens),
([REF]$errors))
if ($errors) {
$msg = "File '{0}' has {1} parser errors." -f $item.FullName,
$errors.Count
Write-Warning $msg
}
:tokenLoop foreach ($token in $tokens) {
if ($token.Kind -ne 'Function') {
continue
}
$position = $token.Extent.StartLineNumber
do {
if (-not $foreach.MoveNext()) {
break tokenLoop
}
$token = $foreach.Current
} until ($token.Kind -in #('Generic', 'Identifier'))
$functionPosition = [pscustomobject]#{
Name = $token.Text
LineNumber = $position
Path = $item.FullName
}
Add-Member -InputObject $functionPosition `
-TypeName FunctionPosition -PassThru
}
}
}
catch {
throw
}
}
}
A label example from Windows Powershell in Action. Labels don't come up that often. In your example, it's breaking out of both the do loop and the token loop with the label.
# loop label, break out of both loops
$target = 'outer'
:outer while (1) {
while(1) {
break $target # break or continue label
}
}

Initialize hashtable from array of objects?

I'm brand new to powershell, as in less than a day experience. I have an array of objects returned from a Get-ADUser call. I will be doing a lot of lookups so thought it best to build a hashtable from it.
Is there a shorthand way to initialize the hashtable with this array and specify one of the object's attributes to use as a key?
Or do I have to loop the whole array and manually add to the set?
$adSet = #{}
foreach ($user in $allusers) {
$adSet.add($user.samAccountname, $user)
}
[...] do I have to loop
Yes
... the whole array ...
No
You don't have to materialize an array and use a loop statement (like foreach(){...}), you can use the pipeline to turn a stream of objects into a hashtable as well, using the ForEach-Object cmdlet - this might prove faster if the input source (in the example below, that would be Get-Service) is slow:
$ServiceTable = Get-Service |ForEach-Object -Begin { $ht = #{} } -Process { $ht[$_.Name] = $_ } -End { return $ht }
The block passed as -Begin will execute once (at the beginning), the block passed to -Process will execute once per pipeline input item, and the block passed to -End will execute once, after all the input has being recevied and processed.
With your example, that would look something like this:
$ADUserTable = Get-ADUser -Filter * |ForEach-Object -Begin { $ht = #{} } -Process { $ht[$_.SAMAccountName] = $_ } -End { return $ht }
Every single "cmdlet" in PowerShell maps onto this Begin/Process/End lifecycle, so generalizing this pattern with a custom function is straightforward:
function New-LookupTable {
param(
[Parameter(Mandatory, ValueFromPipeline)]
[array]$InputObject,
[Parameter(Mandatory)]
[string]$Property
)
begin {
# initialize table
$lookupTable = #{}
}
process {
# populate table
foreach($object in $InputObject){
$lookupTable[$object.$Property] = $object
}
}
end {
return $lookupTable
}
}
And use like:
$ADUserTable = Get-ADUser |New-LookupTable -Property SAMAccountName
See the about_Functions_Advanced document and related help topics for more information about writing advanced and pipeline-enabled functions

PS Object unescape character

I have small error when running my code. I assign a string to custom object but it's parsing the string by itself and throwing an error.
Code:
foreach ($item in $hrdblistofobjects) {
[string]$content = Get-Content -Path $item
[string]$content = $content.Replace("[", "").Replace("]", "")
#here is line 43 which is shown as error as well
foreach ($object in $listofitemsdb) {
$result = $content -match $object
$OurObject = [PSCustomObject]#{
ObjectName = $null
TestObjectName = $null
Result = $null
}
$OurObject.ObjectName = $item
$OurObject.TestObjectName = $object #here is line 52 which is other part of error
$OurObject.Result = $result
$Resultsdb += $OurObject
}
}
This code loads an item and checks if an object exists within an item. Basically if string part exists within a string part and then saves result to a variable. I am using this code for other objects and items but they don't have that \p part which I am assuming is the issue. I can't put $object into single quotes for obvious reasons (this was suggested on internet but in my case it's not possible). So is there any other option how to unescape \p? I tried $object.Replace("\PMS","\\PMS") but that did not work either (this was suggested somewhere too).
EDIT:
$Resultsdb = #(foreach ($item in $hrdblistofobjects) {
[string]$content = Get-Content -Path $item
[string]$content = $content.Replace("[", "").Replace("]", "")
foreach ($object in $listofitemsdb) {
[PSCustomObject]#{
ObjectName = $item
TestObjectName = $object
Result = $content -match $object
}
}
}
)
$Resultsdb is not defined as an array, hence you get that error when you try to add one object to another object when that doesn't implement the addition operator.
You shouldn't be appending to an array in a loop anyway. That will perform poorly, because with each iteration it creates a new array with the size increased by one, copies all elements from the existing array, puts the new item in the new free slot, and then replaces the original array with the new one.
A better approach is to just output your objects in the loop and collect the loop output in a variable:
$Resultsdb = foreach ($item in $hrdblistofobjects) {
...
foreach ($object in $listofitemsdb) {
[PSCustomObject]#{
ObjectName = $item
TestObjectName = $object
Result = $content -match $object
}
}
}
Run the loop in an array subexpression if you need to ensure that the result is an array, otherwise it will be empty or a single object when the loop returns less than two results.
$Resultsdb = #(foreach ($item in $hrdblistofobjects) {
...
})
Note that you need to suppress other output on the default output stream in the loop, so that it doesn't pollute your result.
I changed the match part to this and it's working fine $result = $content -match $object.Replace("\PMS","\\PMS").
Sorry for errors in posting. I will amend that.

Unable to Create Object in Foreach Loop in PowerShell

I am trying to create object using the foreach loop in PowerShell. Tried using "while" loop, it failed as well. Apparently, the looping methods are not allowing me to create objects...
Without further ado...
I have two scripts - Class.psm1 and Main.ps1.
On Class.psm1
Class Car {
[string]$brand
[string]$model
[string]$color
#Constructor
Car ([string]$brand) {
$this.brand = $brand
switch -wildcard ($this.brand) {
('Toyota') {$this.model = 'ABC'; $this.color = 'red'; break}
('Honda') {$this.model = 'FGH'; $this.color = 'blue'; break}
}
}
}
And on Main.ps1
Using module ".\Class.psm1"
$AllCars = {'Toyota', 'Honda'}
[array]$Objects = #()
foreach ($car in $AllCars) {
$temp = New-Object Car("$car")
$Objects += $temp
}
The output from Main.ps1, is that $Objects are just returning back "Toyota" and "Honda", instead of objects (and the properties it supposed to have).
However, if I were to just create the object individually, it will works fine.
For example:
$temp = New-Object Car('Toyota')
$Objects += $temp
$temp = New-Object Car('Honda')
$Objects += $temp
However, this is too manual work or rather unpractical.
May I know in which area did the codes went wrong...? How do I create the objects within the loop?
This issue is you are using {'Toyota', 'Honda'} instead of ('Toyota', 'Honda')
{'Toyota', 'Honda'} is a code block. When you pass it to New-Object Car("$car") it is actually passing New-Object Car("'Toyota', 'Honda'")
$AllCars = ('Toyota', 'Honda')
[array]$Objects = #()
foreach ($car in $AllCars) {
$temp = New-Object Car("$car")
$Objects += $temp
}
Since i was asked why the kangaroo code I decided to post a shorter response
$Objects = 'Toyota', 'Honda' | %{
New-Object Car("$car")
}

Unable to iterate through e-mail objects using powershell in Outlook

I'm writing a script, which takes Outlook folder as input and moves every unread mail to different folder. My code:
Add-Type -assembly "Microsoft.Office.Interop.Outlook"
$Outlook = New-Object -ComObject Outlook.Application
$namespace = $Outlook.GetNameSpace("MAPI")
$olFolderInbox = 6
$inbox = $namespace.GetDefaultFolder($olFolderInbox)
$myFolder = $namespace.pickfolder()
$toFolder = $inbox.Folders | where-object { $_.name -eq "UnreadMessages" }
$messages = $myFolder.Items
$messageCount = $messages.count
for ($i = $messageCount - 1; $i -ge 0; $i--)
{
if ($messages[$i].unread -eq $True)
{
$message.move($toFolder)
}
}
The problem is, that I can not iterate through "messages" objects. Error:
Unable to index into an object of type System.__ComObject.
If it's not possible, then how am I supposed to do it?
Thanks in advance.
EDIT. It's my first day using powershell :)
All Outlook collections are 1 based, not 0 - you need to iterate from Items.Count down to 1.
Secondly, do not just iterate through all messages in a folder, use Items.Find/FindNext. In your case, the search criteria would be "[Unread] = true".
Try to do a get-member on it, and see what your options are.
$Messages | Get-Member -force
If you figure out how you want to proceed, then you can use the ForEach-Object to loop through $Messages' content like this
$Messages | ForEach-Object { Script code }
Or use its abbreviation:
$Messages | % { Script code }
While you cannot use PowerShell's usual indexing syntax - $messages[$i] -
for accessing the elements of $messages, you can use .Items($i) on the folder object (see the docs):
Additionally, as noted in Dmitry Streblechenko's helpful answer, indices start at 1, not 0:
for ($i = $myFolder.Items.Count; $i -ge 1; $i--)
{
$message = $myFolder.Items($i)
if ($message.unread)
{
$message.move($toFolder)
}
}
For simple forward enumeration without indices you can use foreach:
foreach ($message in $myFolder.Items) {
if ($message.unread)
{
$message.move($toFolder)
}
}
($myFolder.Items | ForEach-Object { ... } should work too, but it will be slower.)
That said, Dmitry's answer answer also points to how to perform a filtered enumeration at the source, which performs much better.
Thanks for replying. All your answers were helpful. I finally used foreach loop. While I was trying to use $message = $myFolder.Items($i) following error appeared:
Method invocation failed because [System.__ComObject] doesn't cont
ain a method named 'Items'
so there is no possibility to iterate using indexes in this case.
Thank You all for your time :)