I am trying to cast based on the value of a variable, ultimately to be able to actually test to see if $castFrom is of the $castTo type. I can of course do it with a switch like this
$castTo = '[xml]'
$castFrom = #"
<Settings>
<MachineLogFileArchiveFolder></MachineLogFileArchiveFolder>
</Settings>
"#
switch ($castTo) {
'[int]' {
$castResult = [int]$castFrom
}
'[xml]' {
$castResult = [xml]$castFrom
}
}
But that's a little ugly. What I really want too do is something more like this
$castResult = [($castTo)]$castFrom
or this
$castResult = [$($castTo)]$castFrom
but I am getting the impression the switch really is my only option.
Maybe you could use the ToType method:
$castResult = $castFrom.ToType($castTo, [System.Globalization.DateTimeFormatInfo]::CurrentInfo)
Somehow this doesn't work for your example but could be a starting point.
However, you can do this using the Invoke-Expression cmdlet:
$castResult = Invoke-Expression -Command ('[{0}]$castFrom' -f $castTo)
The -as and -is operators just seem simpler for this situation.
$castto = 'string'
$castfrom = 234
$castfrom.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
$castresult = $castfrom -as $castto
$castresult.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
$castfrom -is $castto
False
$castresult -is $castto
True
Related
I have a need/want to return a [System.Collections.Generic.List[string]] from a function, but it is being covered into a System.Object[]
I have this
function TestReturn {
$returnList = New-Object System.Collections.Generic.List[string]
$returnList.Add('Testing, one, two')
return ,#($returnList)
}
$testList = TestReturn
$testList.GetType().FullName
which returns it as a System.Object[], and if I change the return line to
return [System.Collections.Generic.List[string]]$returnList
or
return [System.Collections.Generic.List[string]]#($returnList)
it returns a [System.String] when there is one item in the list, and a System.Object[] if there is more than one item, in both cases. Is there something odd with a list that it can't be used as a return value?
Now, oddly (I think) it DOES work if I type the variable that receives the value, like this.
[System.Collections.Generic.List[string]]$testList = TestReturn
But that seems like some weird coercion and it doesn't happen with other data types.
If you remove the array subexpression #(...) and just precede with a comma. The below code seems to work:
function TestReturn {
$returnList = New-Object System.Collections.Generic.List[string]
$returnList.Add('Testing, one, two')
return , $returnList
}
$testList = TestReturn
$testList.GetType().FullName
Note: technically this causes the return of [Object[]] with a single element that's of type [System.Collections.Generic.List[string]]. But again because of the implicit unrolling it sort of tricks PowerShell into typing as desired.
On your later point, the syntax [Type]$Var type constrains the variable. It's basically locking in the type for that variable. As such subsequent calls to .GetType() will return that type.
These issues are due to how PowerShell implicitly unrolls arrays on output. The typical solution, somewhat depending on the typing, is to precede the return with a , or ensure the array on the call side, either by type constraining the variable as shown in your questions, or wrapping or casting the return itself. The latter might look something like:
$testList = [System.Collections.Generic.List[string]]TestReturn
$testList.GetType().FullName
To ensure an array when a scalar return is possible and assuming you haven't preceded the return statement with , , you can use the array subexpression on the call side:
$testList = #( TestReturn )
$testList.GetType().FullName
I believe this answer deals with a similar issue
In addition to Steven's very helpful answer, you also have the option to use the [CmdletBinding()] attribute and then just call $PSCmdlet.WriteObject. By default it will preserve the type.
function Test-ListOutput {
[CmdletBinding()]
Param ()
Process {
$List = New-Object -TypeName System.Collections.Generic.List[System.String]
$List.Add("This is a string")
$PSCmdlet.WriteObject($List)
}
}
$List = Test-ListOutput
$List.GetType()
$List.GetType().FullName
For an array, you should specify the type.
function Test-ArrayOutput {
[CmdletBinding()]
Param ()
Process {
[Int32[]]$IntArray = 1..5
$PSCmdlet.WriteObject($IntArray)
}
}
$Arr = Test-ArrayOutput
$Arr.GetType()
$Arr.GetType().FullName
By default the behaviour of PSCmdlet.WriteObject() is to not enumerate the collection ($false). If you set the value to $true you can see the behaviour in the Pipeline.
function Test-ListOutput {
[CmdletBinding()]
Param ()
Process {
$List = New-Object -TypeName System.Collections.Generic.List[System.String]
$List.Add("This is a string")
$List.Add("This is another string")
$List.Add("This is the final string")
$PSCmdlet.WriteObject($List, $true)
}
}
Test-ListOutput | % { $_.GetType() }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
True True String System.Object
True True String System.Object
(Test-ListOutput).GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
# No boolean set defaults to $false
function Test-ListOutput {
[CmdletBinding()]
Param ()
Process {
$List = New-Object -TypeName System.Collections.Generic.List[System.String]
$List.Add("This is a string")
$List.Add("This is another string")
$List.Add("This is the final string")
$PSCmdlet.WriteObject($List)
}
}
Test-ListOutput | % { $_.GetType() }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
(Test-ListOutput).GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True List`1 System.Object
Just wanted to add some information on what I usually use in functions and how to control the behaviour.
How to write function which get array as object from pipeline (actually any object) without to iterate over its items and another one argument (positional). Goal is to modify container (array), but not their items. Something like:
function xxx {
Param( magic-specification??? )
if ($obj type is array) {
iterate over $obj items {
$res = executes $args[0] script-block over them ($_)
if ($res) modify $obj
}
else if ($obj type is object) {
iterate over $obj properties {
$res = executes $args[0] script-block over them ($_)
if ($res) modify $obj property
}
}
here's a demo of how to bypass the way that PoSh is intended to unroll items sent across the pipeline ...
,#(1,2,3) | ForEach-Object {$_.GetType(); "$_" } | Out-Host
#(1,2,3) | ForEach-Object {$_.GetType(); "$_" } | Out-Host
1,2,3 | ForEach-Object {$_.GetType(); "$_" } | Out-Host
output ...
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
1 2 3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
1
True True Int32 System.ValueType
2
True True Int32 System.ValueType
3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
1
True True Int32 System.ValueType
2
True True Int32 System.ValueType
3
note that the 1st one has passed thru an array while the others all unrolled the array. that leading , is the array operator and it causes PoSh to send the array wrapped in another array. the outer one gets unrolled, the inner one gets passed on as an array.
Consider this code:
$a = '[{"a":"b"},{"c":"d"}]'
"Test1"
$a | ConvertFrom-Json | ForEach-Object { $_.GetType() }
"Test2"
$b = $a | ConvertFrom-Json
$b | ForEach-Object { $_.GetType() }
This produces the following output:
Test1
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Test2
True False PSCustomObject System.Object
True False PSCustomObject System.Object
Evidently, if we use a temporary variable, whatever is passed down the pipeline is not the same thing that is passed if we do not use one.
I would like to know what the rules that powershell uses for automatic array wrapping / unwrapping are, and if the using a temp var the best course of action if we need to iterate through a json array.
Update 1
Logically ConvertFrom-Json should return an array with the input given and ForEach-Object should iterated on the said array. However in the first test this does not happen. Why?
Update 2
Is it possible that it's ConvertFrom-Json specific? Like bug/issue?
There is only one rule with regard to pipeline items' unwrapping: all arrays and collections written to the pipeline are always getting unwrapped to the items ("unwrapped one level down" or "unwrapped in a non-recursive fashion" would be more correct statement but for the sake of simplicity we are not going to consider nested arrays so far).
There is still a possibility to override this behavior by using unary comma operator:
$a = 1,2
$a | ForEach-Object{ $_.GetType() }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
True True Int32 System.ValueType
,$a | ForEach-Object{ $_.GetType() }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
In the second case Powershell pipeline engine unwrapped $a but afterwards the result got wrapped back to array by , operator.
As to ConvertFrom-Json case I personally believe that its observed behavior is more predictable as it allows you to capture JSON arrays as a whole by default.
If you are interested in the details, the function Get-WrappedArray in the code below imitates ConvertFrom-Json's behavior:
function Get-WrappedArray {
Begin { $result = #() }
Process { $result += $_ }
End { ,$result }
}
$a | Get-WrappedArray | ForEach-Object{ $_.GetType() }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
$b = $a | Get-WrappedArray
$b | ForEach-Object{ $_.GetType() }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
True True Int32 System.ValueType
I have a snippet of code that opens a word template, then attempts to set values of named FormFields.
$options = #{
'foo' ='bar';
'bizz' = 'buzz';
}
$document = 'C:\Form_template.doc'
$word = new-object -ComObject Word.application
$doc = $word.Documents.Open($document)
$word.visible = $true
$fields = $doc.FormFields
$fields.item('foo').Result = $options['foo']
$fields.item('bizz').Result = $options['bizz']
When running this snippet, the form fields are not set properly. However, when I run
$fields.item('foo').Result = 'bar'
$fields.item('bizz').Result = 'buzz'
The values are set as desired.
Edit: Here's an example in Interactive shell
PS C:\>$fields.item('foo').Result = $options['foo']
PS C:\>$fields.item('bizz').Result = $options['bizz']
PS C:\> $doc.FormFields.Item('foo').Result
PS C:\> $doc.FormFields.Item('bizz').Result
PS C:\>#Now let's try setting the values directly with a string.
PS C:\>$fields.item('foo').Result = 'bar'
PS C:\>$fields.item('bizz').Result = 'buzz'
PS C:\> $doc.FormFields.Item('foo').Result
bar
PS C:\> $doc.FormFields.Item('bizz').Result
buzz
Why am I not able to set the FormField values using values from the hash?
Per a suggestion from Ben casting the string with [string]$options['bizz'] resulted in setting the value correctly.
PS C:\>$fields.item('bizz').Result = [string]$options['bizz']
PS C:\> $doc.FormFields.Item('foo').Result
buzz
Upon further investigation I found that casting the hash value to string returned a different type vs using .toString()
PS C:\> $options['bizz'].getType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
PS C:\> $options['bizz'].toString().getType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
PS C:\> [string]$options['bizz'].getType()
string
I'm interested in why that is, but that would be a topic for another thread. Proper casting resolved my issue.
So i have folder with several files:
$files = #(Get-ChildItem "myPath")
I can see via the debugger that $files contains several items and i want to take the first:
$files[0] = "123_this.is.string"
And i want to split in by '_' and take 123
$splitted = $files[0] -split "_"
So here i can see that $splitted is empty.
Any suggestions why this strange behavior ?
$files[0] isn't a string but a FileSystemInfo Object.
$files[0].getType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True FileInfo System.IO.FileSystemInfo
So to get it work you have to use the split function to the filename of the file which is a string.
$files[0].name.getType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
With this it should work:
$files[0].name.split("_")
Try:
$files[0].ToString().split("_")