Change property on existing powershell object - powershell

I have a custom powershell object created with a function:
$obj = myfunction -name "hello" -value 5
After it's created I want to change the value property however doing it (demonstrated below) doesn't work
$obj.value = 1
I've searched and can't seem to find anything - can anybody explain how I can accomplish this?
Here is my function that creates an returns the object
function myfunction
{
[CmdletBinding()]
[OutputType([System.Collections.Hashtable])]
Param
(
[Parameter(Mandatory=$true,
Position=0)]
[String]
$name,
[Parameter(Mandatory=$true,
Position=1)]
[int]
$value,
)
Process
{
$myfunction = #{
name = $name
value = $value
}
write-output $myfunction
}
}

If you're returning a hashtable, you should be able to do what you wrote:
PS> $obj = myfunction -name "hello" -value 5
PS> $obj.value = 1
PS> $obj
Name Value
---- -----
name hello
value 1
However, the "safe" way is to use square braces.
You might want to try that, because my gut is that you tried to set .Values instead of .Value ... and .Values is a non-settable property of Hashtables.
PS> $obj = myfunction -name "hello" -value 5
PS> $obj["value"] = 1
PS> $obj
Name Value
---- -----
name hello
value 1
Either way, you could avoid all that by creating an actual PSCustomObject as #jaqueline-vanek did in her answer.

PS C:\Users\joshua> $obj = [PSCustomObject]#{ hello = 5 }
PS C:\Users\joshua> $obj.hello
5
PS C:\Users\joshua> $obj.hello = 1
PS C:\Users\joshua> $obj.hello
1
PowerShell: Creating Custom Objects

The OP is correct it does not work.
It should be,
$obj."Hello" to access the property value
$obj."Hello" = 1 to set the property.
You have to double quote the property name.

Related

In Powershell, how do I convert an array of PSObjects to an array of Strings?

My code is as follows:
#Creating object with data name properties
$myData = New-Object -TypeName psobject
$myData | Add-Member -MemberType NoteProperty -Name Name -Value $null
$myData | Add-Member -MemberType NoteProperty -Name Bot -Value $null
$myData | Add-Member -MemberType NoteProperty -Name PDD -Value $null
$myData | Add-Member -MemberType NoteProperty -Name SD -Value $null
$myData | Add-Member -MemberType NoteProperty -Name Dev -Value $null
#Empty ArrayList to Populate
$InputArray = #()
for ($i = 2; $i -le $rowMax; $i++)
{
$objTemp = $myData | Select-Object *
#Reading row data
$objTemp.Name = $sheet.Cells.Item($i,1).Text
$objTemp.Bot = $sheet.Cells.Item($i,2).Text
$objTemp.PDD = $sheet.Cells.Item($i,3).Text
$objTemp.SD = $sheet.Cells.Item($i,4).Text
$objTemp.Dev = $sheet.Cells.Item($i,5).Text
$InputArray += $objTemp
}
foreach ($i in $InputArray)
{
if ($i.Name -eq $CurrentName) {
#want to convert these name properties to strings
$Name = $i.Name
$Bot = $i.Bot
$PDD = $i.PDD
$Dev = $i.Dev
}
}
The code above builds the PSobject object with several name properties that are read in from an excel sheet. After that, I am reading in each psobject in $InputArray, and targeting the properties of the current array in that index.
The problem I'm running into is I need to convert the property values (Name, Bot, PDD, SD, Dev) into string values.
I've tried a few methods to no avail, any input would be much appreciated
[string] $Name = $i.Name would store the stringified value of $i.Name in variable $Name, for instance - although it's surprising that that is needed, given that you're accessing a property named .Text on the Excel cell objects.
Generally, there are two basic ways to stringify (convert a value to a string) in PowerShell; assume the following two variable definitions in the examples below:
# Sample values to stringify.
$val = 1.2 # [double]
$coll = 42, 1.2 # array, containing [int] and [double]
With PowerShell custom logic: culture-invariant, with additional logic for meaningfully stringifying collections, such as arrays:
Via a [string] cast or type constraint:
[string] 1.2 # -> "1.2", irrespective of the current culture.
# In combination with variables:
$valAsString = [string] 1.2 # cast
[string] $valAsString = 1.2 # type-constraint; auto-converts future
# assignments to [string] too
[string] $coll # -> "42 1.2", i.e. the space-concatenated list
# of the (themselves stringified) elements.
Via an expandable (interpolating) string, i.e. inside "...":
# Note: Only simple variable references as used here can *directly*
# be embedded in "..."; to embed *expressions or commands*,
# you must use $(...), the subexpression operator; e.g.:
# "$($val+1)" # -> "2.2"
"$val" # same as: [string] $val
"$coll" # same as: [string] $coll
Via the .NET type at hand: potentially culture-sensitive:
Explicitly, via its .ToString() method:
$val.ToString() # -> "1.2" in English cultures,
# "1,2" in French, for instance
$coll.ToString() # -> "System.Object[]", i.e. just the *type name*
Implicitly, via -f, the format operator:
'val: {0}' -f $val # -> "val: 1.2" in English cultures,
# "val: 1,2" in French, for instance
See this answer for more information.
Also note that PowerShell's flexible type conversions perform stringification on demand, e.g. when passing a non-string to a [string]-typed parameter.
As for what you tried:
Your code can be greatly simplified as follows; the source-code comments provide pointers, but explaining every optimization would be beyond the scope of this answer:
# Let PowerShell collect the loop output in an array for you,
$inputArray =
foreach ($i in 2..$rowMax) {
# Construct and output a [pscustomobject] via a literal.
# Note: If desired, you could apply [string] casts *here*; e.g.:
# Name = [string] $sheet.Cells.Item($i,1).Text
[pscustomobject] #{
Name = $sheet.Cells.Item($i,1).Text
Bot = $sheet.Cells.Item($i,2).Text
PDD = $sheet.Cells.Item($i,3).Text
SD = $sheet.Cells.Item($i,4).Text
Dev = $sheet.Cells.Item($i,5).Text
}
}
# ...
foreach ($i in $InputArray) {
if ($i.Name -eq $CurrentName) {
# want to convert these name properties to strings
[string] $Name = $i.Name
[string] $Bot = $i.Bot
[string] $PDD = $i.PDD
[string] $Dev = $i.Dev
}
# ...
}

Generated string names in Calculated Properties aren't accepted by select cmdlet

I want to generate the following table:
AAA BBB CCC
--- --- ---
10 10 10
10 10 10
10 10 10
10 10 10
10 10 10
So I write the following code using a foreach loop to generate the column names:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = $_; expression = { 10 } }
}
#(1..5) | select -Property $property
But I get the following error saying the name is not a string:
select : The "name" key has a type, System.Management.Automation.PSObject, that is not valid; expected type is System.String.
At line:4 char:11
+ #(1..5) | select -Property $property
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Select-Object], NotSupportedException
+ FullyQualifiedErrorId : DictionaryKeyIllegalValue2,Microsoft.PowerShell.Commands.SelectObjectCommand
To get the code work, I have to convert the $_ to string like below:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = [string]$_; expression = { 10 } }
}
#(1..5) | select -Property $property
Or like below:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = $_; expression = { 10 } }
}
$property | foreach { $_.name = [string]$_.name }
#(1..5) | select -Property $property
The question is: the $_ is already a string. Why do I have to convert it to string again? And why select thinks that the name is PSObject?
To confirm that it's already a string, I write the following code to print the type of name:
$property = #('AAA', 'BBB', 'CCC') | foreach {
#{ name = $_; expression = { 10 } }
}
$property | foreach { $_.name.GetType() }
The following result confirms that it's already a string:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
True True String System.Object
True True String System.Object
I know that there are many other easier ways to generate the table. But I want to understand why I have to convert a string to string to make the code work, and why select doesn't think that the string is a string. For what it's worth, my $PSVersionTable.PSVersion is:
Major Minor Build Revision
----- ----- ----- --------
5 1 18362 1474
You're seeing the unfortunate effects of incidental, normally invisible [psobject] wrappers PowerShell uses behind the scenes.
In your case, because the input strings are supplied via the pipeline, they get wrapped in and stored as [psobject] instances in your hashtables, which is the cause of the problem.
The workaround - which is neither obvious nor should it be necessary - is to discard the wrapper by accessing .psobject.BaseObject:
$property = 'AAA', 'BBB', 'CCC' | ForEach-Object {
#{ name = $_.psobject.BaseObject; expression = { 10 } }
}
1..5 | select -Property $property
Note:
In your case, a simpler alternative to .psobject.BaseObject (see the conceptual about_Intrinsic Members help topic) would have been to call .ToString(), given that you want a string.
To test a given value / variable for the presence of such a wrapper, use -is [psobject]; with your original code, the following yields $true, for instance:
$property[0].name -is [psobject]
Note, however, that this test is meaningless for [pscustomobject] instances, where it is always $true (custom objects are in essence [psobject] instances without a .NET base objects - they only have dynamic properties).
That the normally invisible [psobject] wrappers situationally, obscurely result in behavioral differences is arguably a bug and the subject of GitHub issue #5579.
Simpler and faster alternative, using the .ForEach() array method:
$property = ('AAA', 'BBB', 'CCC').ForEach({
#{ name = $_; expression = { 10 } }
})
1..5 | select -Property $property
Unlike the pipeline, the .ForEach() method does not wrap $_ in [psobject], so the problem doesn't arise and no workaround is needed.
Using the method is also faster, although note that, unlike the pipeline, it must collect all its input in memory up front (clearly not a problem with an array literal).

PowerShell wait for output from function or invoke-command

I have a script block/function that returns PSCustomObject followed by Write-Host.
I want to get the output first then print the write-host but I can't seem to figure it out.
function ReturnArrayList {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return $folderList
}
$sb = {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return $folderList
}
ReturnArrayList -number 5
#Invoke-Command -ScriptBlock $sb -ArgumentList 5
Write-Host "This write host should come later"
Result:
This write host should come after
Name number
---- ------
John 5
Desired result:
Name number
---- ------
John 5
This write host should come after
How can I get the return result first and print the write-host message?
Thank you for your help in advance!
You can force PowerShell to write the output from ReturnArrayList to the screen before reaching Write-Host by piping it to either one of the Format-* cmdlets or Out-Default:
$object = ReturnArrayList -number 5
$object |Out-Default
Write-Host "This write host should come later"
Result:
Name number
---- ------
John 5
This write host should come later
Beware that your ReturnArrayList function does not actually return an ArrayList - PowerShell will automatically enumerate the item(s) in $folderlist, and since it only contains one item, the result is just a single PSCustomObject, "unwrapped" from the ArrayList so to speak:
PS ~> $object = ReturnArrayList -number 5
PS ~> $object.GetType().Name
PSCustomObject
To preserve enumerable types as output from functions, you'll need to either use Write-Output -NoEnumerate, or wrap the it in an array using the , operator:
function ReturnArrayList {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return ,$folderList
# or
Write-Output $folderList -NoEnumerate
}
Data is usually output to the pipeline, while Write-Host bypasses the pipeline and writes to the console directly.
Using Write-Output instead of Write-Host will fix this issue. You can easily find more in-depth information on this topic, and when not to Write-Host.

Weird Object[] casting from PSObject through a function call

I'm currently coding a function able to cast an ADO (EML attachments from Email) into a PSObject. A stub of the code looks like this:
function Get-OriginalMailAttributes([__ComObject]$email){
#.DESCRIPTION Downloads in Temp the attachment and open it.
# Returns PSObjectwith values.
#STEP 1: Download EML
$TEMPFILE = "..."
if($email.Attachments.Count){
$attachment=$email.Attachments|?{$_.Filename.endsWith(".eml")} | select -First 1
$fileName = $TEMPFILE + $(New-Guid | select -exp Guid) + ".eml"
$attachment.SaveAsFile($fileName)
#STEP2 : Retrieve EML Objects
$adoDbStream = New-Object -ComObject ADODB.Stream
$adoDbStream.Open()
$adoDbStream.LoadFromFile($fileName)
$cdoMessage = New-Object -ComObject CDO.Message
$cdoMessage.DataSource.OpenObject($adoDbStream, "_Stream")
#STEP 3: Bind values
$attributes = New-Object PSObject -Property #{
From = [string]($cdoMessage.Fields.Item("urn:schemas:mailheader:from").Value.toString())
}
#STEP 4: Cleanup
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($cdoMessage)
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($adoDbStream)
Remove-Item $fileName
return $attributes
# Note that the debugger acknowledge the fact that $attributes is PSObject.
}
}
I'm then calling the function at some point:
$sender_raw = Get-OriginalMailAttributes $email
$sender_raw.getType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
$sender_raw
0
0
From
----
"Bob" <dummy#contoso.com>
$sender_raw.From # Crashes.
Where does this behavior come from?
Typically when you have "extra" values in the output from a function it's because you're calling methods that have return values and not dealing with those values.
.Add() methods are notorious for outputting the index of the item that was added.
To avoid this problem, you have a few options:
Cast to void: [void]$collection.Add($item)
Assign to $null: $null=$collection.Add($item)
Pipe to out-null: $collection.Add($item) | out-null
I don't see any obvious methods in your code, but adding Write-output 'Before Step 1' etc. throughout the code should make it clear where the offending statements are.
This weird behavior comes from multi-returns from PowerShell functions.
PS> function f() {
1+1
return 4
}
PS> f
2
4
In short, if one-liners are returning values, they are added as an Object[] array to the output.
While this is a particularly chaotic way to handle return values of a function, my solution on my code will be to redirect output NULL:
stub...
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($cdoMessage) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($adoDbStream) | Out-Null
See Suppressing return values in PowerShell functions

Is it possible to use the value of a variable, as a new variable name?

Long time listener, first time caller! :) So I FEEL like this is possible, it's just that the solution eludes me.
I'm setting a variable ($thisName = "a") and using that to create an object.
I'd LIKE to use $thisName as, not only a property, but also the name of the object.
IS this possible?
$thisName = "a"
$theseParams = #{ Name = $thisName; DebugMode = $true }
$thisName = New-Object [PSCustomObject] -Property #theseParams
I'm WANTING the variable name of the object to be $a, as well as the $a.Name to be "a". But I'm just resetting (or outright breaking) $thisName.
I believe New-Variable may help:
$thisName = "a"
$theseParams = #{ Name = $thisName; DebugMode = $true }
New-Variable -Name $thisName -Value $theseParams
The output seems to be what you're asking for:
PS C:\> $a.Name
a
PS C:\> $a
Name Value
---- -----
DebugMode True
Name a
Hope this helps.