Powershell: PSCustomObject array as parameter in function gets changed unexpectedly - arrays

In the simplified PS code below, I don't understand why the $MyPeople array gets changed after calling the changeData function. This array variable should just be made a copy of, and I expect the function to return another array variable into $UpdatedPeople and not touch $MyPeople:
function changeData {
Param ([PSCustomObject[]]$people)
$changed_people = $people
$changed_people[0].Name = "NEW NAME"
return $changed_people
}
# Original data:
$Person1 = [PSCustomObject]#{
Name = "First Person"
ID = 1
}
$Person2 = [PSCustomObject]#{
Name = "Second Person"
ID = 2
}
$MyPeople = $Person1,$Person2
"`$MyPeople[0] =`t`t" + $MyPeople[0]
"`n# Updating data..."
$UpdatedPeople = changeData($MyPeople)
"`$UpdatedPeople[0] =`t" + $UpdatedPeople[0]
"`$MyPeople[0] =`t`t" + $MyPeople[0]
Console output:
$MyPeople[0] = #{Name=First Person; ID=1}
# Updating data...
$UpdatedPeople[0] = #{Name=NEW NAME; ID=1}
$MyPeople[0] = #{Name=NEW NAME; ID=1}
Thanks!

PSObject2 = PSObject1 is not a copy but a reference. You need to clone or copy the original object using a method designed for that purpose.
function changeData {
Param ([PSCustomObject[]]$people)
$changed_people = $people | Foreach-Object {$_.PSObject.Copy()}
$changed_people[0].Name = "NEW NAME"
return $changed_people
}
The technique above is simplistic and should work here. However, it is not a deep clone. So if your psobject properties contain other psobjects, you will need to look into doing a deep clone.

We can clone the PSCustomObject. We will create a new PSObject and enumerate through the psobject given as parameter and add them one by one to the shallow copy.
function changeData {
Param ([PSCustomObject[]]$people)
$changed_people = New-Object PSobject -Property #{}
$people.psobject.properties | ForEach {
$changed_people | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $_.Value
}
$changed_people[0].Name = 'NEW NAME'
return $changed_people
}
Or use another method by #AdminOfThings

Related

Populate combox with mapped drives

Trying to populate a combo box with mapped drive letters (name) and FQDN (Root). I think I have most of the code but the combo box entries includes coded entries.
I'm not only curious about how to fix this but why the results are entered this way. Running this via command line does not display results this way.
NOTE: I'm also using a function to populate the combo box.
Code to retrieve mapped drives
Load-ComboBox -ComboBox $cboDomain -Items (Get-PSDrive -PSProvider FileSystem | Select-Object name, #{ n = "Root"; e = { if ($_.DisplayRoot -eq $null) { $_.Root } else { $_.DisplayRoot } } })
Function to load combo box
function Load-ComboBox{
Param (
[ValidateNotNull()]
[Parameter(Mandatory=$true)]
[System.Windows.Forms.ComboBox]$ComboBox,
[ValidateNotNull()]
[Parameter(Mandatory=$true)]
$Items,
[Parameter(Mandatory=$false)]
[string]$DisplayMember,
[switch]$Append
)
if(-not $Append)
{
$ComboBox.Items.Clear()
}
if($Items -is [Object[]])
{
$ComboBox.Items.AddRange($Items)
}
elseif ($Items -is [System.Collections.IEnumerable])
{
$ComboBox.BeginUpdate()
foreach($obj in $Items)
{
$ComboBox.Items.Add($obj)
}
$ComboBox.EndUpdate()
}
else
{
$ComboBox.Items.Add($Items)
}
$ComboBox.DisplayMember = $DisplayMember}
The entries look like;
#{Name=C; Root=C:}
#Name=S; Root=\\server\share}
I want it to look like;
C<-tab->C:\
S<-tab->\\server\share
*Sorry couldn't figure out how to actually insert tab
Since you are sending Objects to the function (Select-Object returns objects), and not an array of tab separated strings, the function would work if you call it like this:
$drives = (Get-PSDrive -PSProvider FileSystem | ForEach-Object {
$root = if ($_.DisplayRoot -eq $null) { $_.Root } else { $_.DisplayRoot }
# output a tab-separated string that gets collected in the $drives variable
"$($_.Name)`t$root"
})
Load-ComboBox -ComboBox $cboDomain -Items $drives
Hope that explains

Runspace scriptblock array cannot be passed

I'm tinkering with the code below. It should create a PoshRSJob and run the function foo again in the runspace there.
I want to be able to turn the $list parameter into an [array] or [string[]], but when I do it throws errors. I considered flattening my array into a string, but if I change $list3 to include a space or comma in the string it also throws an error. I believe it is this line that is causing the issue, but I don't know why or what to do to circumvent this issue:
ScriptBlock = [scriptblock]::Create("`$_ | $($PSCmdlet.MyInvocation.MyCommand.Name) -Parallel:`$false -fn:$fn -sqlQuery:$SQLQuery -option:$option -List:$List")
Code:
function foo {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[Alias("ComputerName")]
[PSObject] $InputObject,
[switch] $Parallel = $true,
[string] $fn,
[string] $sqlQuery = "none",
[string] $option = "none",
[int] $number,
[int] $Throttle = 100,
#Want this to be [array] or [string[]] :
[string] $List = "none"
)
begin {
$batch = [System.Guid]::NewGuid().Guid #run all jobs under same batch number
}
process {
if ($Global:debugging -eq $true){$host.ui.WriteDebugLine("fn:$fn | SQLQuery:$sqlQuery")}
if (!$Parallel) {
$server = $InputObject.name
switch($fn){
Manage{ return $list }
} #end switch
} else {#region Parallel run
$jobArguments = #{
Throttle = $Throttle
Batch = $batch
FunctionsToLoad = $PSCmdlet.MyInvocation.MyCommand.Name
#This is the problematic line:
ScriptBlock = [scriptblock]::Create("`$_ | $($PSCmdlet.MyInvocation.MyCommand.Name) -Parallel:`$false -fn:$fn -sqlQuery:$SQLQuery -option:$option -List:$List")
}
if ($_ -and $_ -isnot [string]) { $serverName=$_ } else { $serverName=$InputObject.name }
#(if ($_ -and $_ -isnot [string]) { $_ } else { $InputObject }) | Start-RSJob #jobArguments | Out-Null
} #endregion
}
end {#region Wait for results and return them
if ($Parallel) {
Get-RSJob -batch $batch | Wait-RSJob -ShowProgress | Out-Null
}#endregion
}
}
$obj = New-Object -TypeName PSObject -Property #{
Name = 'server1'
Other = 'other'
}
$list2 = $obj
$list3 = "item1-item2"
$list2 | foo -fn 'Manage' -number 2 -option Q -List $list3
This is the error:
The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
Does anyone know how to get this working so I can pass a list into the runspace?

Powershell turn strings into array with headings

I'd like to create a table with headings from a series of strings, which have been pulled from an output. I've already used this...
$Scopearray = #("$server","$ip","$ScopeName","$Comment")
To turn this...
$ip = $Trimmed[0]
$server = $Trimmed[1]
$ScopeName = $Trimmed[2]
$Comment = $Trimmed[3]
Into this:
PS C:\WINDOWS\system32> $Scopearray
MyServer.domain
10.1.1.1
NameofScope
ScopeDetails
But I need to turn that into a table, something like this:
I've tried the below, and a copule of other multidimentional examples, but I'm clearly missing something fundamental.
$table = #()
foreach ($instance in $Scopearray) {
$row = "" | Select ServerName,IP,ScopeName,Comment
$row.Heading1 = "Server Name"
$row.Heading2 = "IP Address"
$row.Heading3 = "Scope Name"
$row.Heading4 = "Comment"
$table += $row
}
Create objects from your input data:
... | ForEach-Object {
New-Object -Type PSObject -Property #{
'Server Name' = $Trimmed[1]
'IP Address' = $Trimmed[0]
'Scope Name' = $Trimmed[2]
'Comment' = $Trimmed[3]
}
}
In PowerShell v3 and newer you can simplify that by using the [PSCustomObject] type accelerator:
... | ForEach-Object {
[PSCustomObject]#{
'Server Name' = $Trimmed[1]
'IP Address' = $Trimmed[0]
'Scope Name' = $Trimmed[2]
'Comment' = $Trimmed[3]
}
}
PowerShell displays objects with up to 4 properties in tabular form by default (unless the objects have specific formatting instructions), but you can force tabular output via the Format-Table cmdlet if required:
... | Format-Table
Note that you need Out-String in addition to Format-Table if for instance you want to write that tabular representation to a file:
... | Format-Table | Out-String | Set-Content 'C:\output.txt'

Easy way to List info from arrays

I have the code below which checks the registry for entries (more than 20 of them) and if it doesn't exists it creates a registry key and adds it to an array.
After that I need to check for all the names in the array to my other array and if it matches, I need it to pull the info from my second array and show it on the screen(the log location, registry location etc). But Can't really figure out how to match the array and write in on the screen without writing very long if statements.
Does anyone know a good way of doing this?
Thanks in advance!
$Reg = "HKLM:\Software\"
$NeedtoCheck = #()
$testing = #("Test1Name","Test2Name", "Test3Name")
$allTests = #(
$Test1 = #{
Name = "Test1"
Logfile = "C:\Checking\test1.log"
Version = "16"
RegName = "test1Nameinfo*"
Installname = "InstallTest1"
UninstallName = "UninstallTest1"
},
$Test2 = #{
Name = "Test"
Logfile = "C:\test2.log"
Version = "7"
RegName = "test2Nameinfo*"
Installname = "InstallTest2"
UninstallName = "UninstallTest2"
},
$Test3 = #{
Name = "Test3"
Logfile = "C:\Temp\Checkhere\test3.log"
Version = "99"
RegName = "test3Nameinfo*"
Installname = "InstallTest3"
UninstallName = "UninstallTest3"
}
$Test1Name = $Test1.name
$Test1Logfile = $Test1.Logfile
$Test1Version = $Test1.Version
$Test1RegName = $Test1.RegName
$Test1Install = $Test1.InstallName
$Test1Uninstall = $Test1.UninstallName
$Test2Name = $Test2.name
$Test2Logfile = $Test2.Logfile
$Test2Version = $Test2.Version
$Test2RegName = $Test2.RegName
$Test2Install = $Test2.InstallName
$Test2Uninstall = $Test2.UninstallName
$Test3Name = $Test3.name
$Test3Logfile = $Test3.Logfile
$Test3Version = $Test3.Version
$Test3RegName = $Test3.RegName
$Test3Install = $Test3.InstallName
$Test3Uninstall = $Test3.UninstallName
Foreach($Test in $testing){
$Key = (Get-Item "Reg").getvalue("$Test")
IF($Key -eq $null)
{
New-Itemproperty -path "HKLM:\Software\" -value "Check" -PropertyType string -name $Test -Force -ErrorAction SilentlyContinue
Write-Host "$Test created"
$Needtocheck += $Test
}
ELSEIF($key -eq "Check")
{
$Needtocheck += $Test
}
ELSE
{
Write-Host "$Test already Checked"
}
}
Foreach($item in $NeedtoCheck)
{
If($item -match $Test1Name)
{
Write-Host "$Test1Name info"
Write-host "$Test1Name`
$Test1Logfile`
$Test1Version`
$Test1RegName`
$Test1Install`
$Test1Uninstall`
}
Else
{
Write-Host "Not in the list"
}
}
....
This code doesn't make a lot of sense to be honest. If you want 20 checks to be setup, and then only run certain checks, then that's fine, but you really don't need additional cross checking to reference one array against another array, and redefining things like you do when you assign variables for each values in each hashtable. Personally I'd make objects not hashtables, but that's me. Actually, probably even better, make a hashtable with all available tests, then for the value make an object with the properties that you need. Oh, yeah, that'd be the way to go, but would need a little re-writing. Check this out...
$Reg = 'HKLM:\Software\'
$NeedtoCheck = #()
$testing = #('Test2','Test1','NotATest')
#Define Tests
$AllTests = #{'Test1' = [PSCustomObject]#{
Name = "Test1"
Logfile = "C:\Checking\test1.log"
Version = "16"
RegName = "test1Nameinfo*"
Installname = "InstallTest1"
UninstallName = "UninstallTest1"
}
'Test2' = [PSCustomObject]#{
Name = "Test"
Logfile = "C:\test2.log"
Version = "7"
RegName = "test2Nameinfo*"
Installname = "InstallTest2"
UninstallName = "UninstallTest2"
}
'Test3' = [PSCustomObject]#{
Name = "Test3"
Logfile = "C:\Temp\Checkhere\test3.log"
Version = "99"
RegName = "test3Nameinfo*"
Installname = "InstallTest3"
UnnstallName = "UninstallTest3"
}
}
#$allTests = #($Test1,$Test2,$Test3)
Foreach($Test in $Testing){
If($Test -in $allTests.Keys){
$Key = (Get-Item $Reg).getvalue($AllTests[$Test].RegName)
Switch($Key){
#Case - Key not there
{[string]::IsNullOrEmpty($_)}{
New-Itemproperty -path "HKLM:\Software\" -value "Check" -PropertyType string -name $AllTests[$Test].RegName -Force -ErrorAction SilentlyContinue
Write-Host "`n$Test created"
Write-Host "`n$Test info:"
Write-host $allTests[$test].Name
Write-host $allTests[$test].LogFile
Write-host $allTests[$test].Version
Write-host $allTests[$test].RegName
Write-host $allTests[$test].Installname
Write-host $allTests[$test].Uninstallname
}
#Case - Key = 'Check'
{$_ -eq "Check"}{
Write-Host "`n$Test info:`n"
Write-host $allTests[$test].Name
Write-host $allTests[$test].LogFile
Write-host $allTests[$test].Version
Write-host $allTests[$test].RegName
Write-host $allTests[$test].Installname
Write-host $allTests[$test].Uninstallname
}
#Default - Key exists and does not need to be checked
default {
Write-Host "`n$Test already Checked"
}
}
}Else{
Write-Host "`n$Test not in list"
}
}
That should do what you were doing before, with built in responses and checks. Plus this doesn't duplicate efforts and what not. Plus it allows you to name tests whatever you want, and have all the properties you had before associated with that name. Alternatively you could add a member to each test run, like 'Status', and set that to Created, Check, or Valid, then you could filter $AllTests later and look for entries with a Status property, and filter against that if you needed additional reporting.
You can filter down the tests you want to check like so, if I understand what you are asking for:
$Needtocheck | Where {$_ -in $testing} |
Foreach {... do something for NeedToCheck tests that existing in $testing ... }
I had to change several pieces of the code as there were syntax errors. Guessing most were from trying to create some sample code for us to play with. I have many comments in the code but I will explain some as well outside of that.
$Reg = "HKLM:\Software\"
$testing = "Test1","Test2", "Test3"
$allTests = #(
New-Object -TypeName PSCustomObject -Property #{
Name = "Test1"
Logfile = "C:\Checking\test1.log"
Version = "16"
RegName = "test1Nameinfo*"
Installname = "InstallTest1"
UninstallName = "UninstallTest1"
}
New-Object -TypeName PSCustomObject -Property #{
Name = "Test2"
Logfile = "C:\test2.log"
Version = "7"
RegName = "test2Nameinfo*"
Installname = "InstallTest2"
UninstallName = "UninstallTest2"
}
New-Object -TypeName PSCustomObject -Property #{
Name = "Test3"
Logfile = "C:\Temp\Checkhere\test3.log"
Version = "99"
RegName = "test3Nameinfo*"
Installname = "InstallTest3"
UninstallName = "UninstallTest3"
}
)
$passed = $testing | ForEach-Object{
# Changed the for construct to better allow output. Added the next line to make the rest of the code the same.
$test = $_
$Key = (Get-Item $Reg).getvalue($Test)
If($Key -eq $null){
# New-Itemproperty creates output. Cast that to void to keep it out of $passed
[void](New-ItemProperty -path "HKLM:\Software\" -value "Check" -PropertyType string -name $Test -Force -ErrorAction SilentlyContinue)
Write-Host "$Test created"
# Send this test to output
Write-Output $Test
} Elseif ($key -eq "Check")
{
# Send this test to output
Write-Output $Test
} Else {
Write-Host "$Test already Checked"
}
}
$allTests | Where-Object{$passed -contains $_.Name}
We run all the values in $testing and if one is created or already "Checked" then we send it down the pipe where it populates the variable $passed. The we take $allTests and filter out every test that has a match.

How to append to powershell Hashtable value?

I am interating through a list of Microsoft.SqlServer.Management.Smo.Server objects and adding them to a hashtable like so:
$instances = Get-Content -Path .\Instances.txt
$scripts = #{}
foreach ($i in $instances)
{
$instance = New-Object Microsoft.SqlServer.Management.Smo.Server $i
foreach($login in $instance.Logins)
{
$scripts.Add($instance.Name, $login.Script())
}
}
So far so good. What I want to do now is append a string to the end of the hashtable value. So for an $instance I want to append a string to the hashtable value for that $instance. How would I do that? I have started with this, but I'm not sure if I'm on the right track:
foreach ($db in $instance.Databases)
{
foreach ($luser in $db.Users)
{
if(!$luser.IsSystemObject)
{
$scripts.Set_Item ($instance, <what do I add in here?>)
}
}
}
Cheers
$h= #{}
$h.add("Test", "Item")
$h
Name Value
---- -----
Test Item
$h."Test" += " is changed"
$h
Name Value
---- -----
Test Item is changed
I would go with this code.
$instances = Get-Content -Path .\Instances.txt
$scripts = #{}
foreach ($i in $instances)
{
$instance = New-Object Microsoft.SqlServer.Management.Smo.Server $i
foreach($login in $instance.Logins)
{
$scripts[$instance.Name] = #($scripts[$instance.Name]) + $login.Script().ToString()
}
}
.
foreach ($db in $instance.Databases)
{
foreach ($luser in $db.Users)
{
if(!$luser.IsSystemObject)
{
$scripts[$instance] = #($scripts[$instance]) + $luser.Script().ToString()
}
}
}
The result will be a hash table with each instance as a key, and an array of strings where each string is the T-SQL script for a user.
The .Script() method returns a string collection. There's probably a more elegant way of doing it, but replacing
$scripts.Set_Item ($instance, <what do I add in here?>)
with
$val = $scripts[$instance]
$val.Add("text to add")
$scripts.Set_Item($instance, $val)
should work.
$test = #{}
$test.Hello = "Hello World"
Write-Host "message from $($test.Hello)"
$test.Hello += " Cosmonaut"
Write-Host "message from $($test.Hello)"

Resources