In powershell, is there a way to specify that a parameter is generic? Basically, the function I'm writing doesn't care what type you hand it, as it technically works with all objects, but if I specify [object[]] as the parameter type, the objects loose type information which may or may not be handled correctly by other CmdLets down the pipeline.
function Pack-Objects{
[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[object[]]$InputObjects
)
BEGIN{
$OutputObjects = New-Object System.Collections.ArrayList($null)
}PROCESS{
$OutputObjects.Add($_) | Out-Null
}END{
Write-Verbose "Passing off $($OutputObjects.Count) objects downstream"
return ,$OutputObjects.ToArray()
}
}
For example, if I run 1,2,3 | Pack-Objects | Get-Member, the returned type is System.Object[]. I want it so that if I pass it integers, it gives me an array of integers; string and array of strings; and so on.
I don't understand why you need it but this should do what you want:
[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
$InputObjects
)
BEGIN{
$script:i = 0
$script:OutputObjects = New-Object System.Collections.ArrayList($null)
}
PROCESS{
foreach ( $_ in $InputObjects)
{
$script:t = $_.gettype() | select -expa name
if ( $i -gt 0 )
{
if ( $ts -notmatch $t ){"types are differents!!";break}
}
$ts = $t
$i++
$OutputObjects.Add($_) | Out-Null
}
}
END{
iex "[$t[]]`$OutputObjects = `$OutputObjects.toarray()"
#(,$OutputObjects)
}
PS C:\ps> ("q","d","e" | .\Pack-Objects.ps1).gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String[] System.Array
PS C:\ps> (1,2,3 | .\Pack-Objects.ps1).gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32[] System.Array
PS C:\ps> 1,2,3,"e" | .\Pack-Objects.ps1
types are differents!!
Related
The command $output = Invoke-Expression "cfn-lint *.yaml" gives me an array for $output:
$output.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Write-Output $output gives me this nice output:
Write-Output $output
W2506 Parameter AMIID should be of type [AWS::EC2::Image::Id,
AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>]
trace3-ec2-virtual-machine-creation.yaml:27:3
W2001 Parameter VpcId not used.
trace3-ec2-virtual-machine-creation.yaml:32:3
But Write-Host $output gives me a version with all the newlines stripped out:
Write-Host $output
W2506 Parameter AMIID should be of type [AWS::EC2::Image::Id, AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>] trace3-ec2-virtual-machine-creation.yaml:27:3 W2001 Parameter VpcId not used. trace3-ec2-virtual-machine-creation.yaml:32:3
Why is it doing this?
I know that I can get around this with the call Write-Host ($output | Out-String), but I don't understand why that works.
Write-Output takes [-InputObject] <PSObject[]> as argument while Write-Host takes [[-Object] <Object>] as argument.
Write-Output returns the objects that are submitted as input.
Write-Host sends the objects to the host. It does not return any objects. However, the host displays the objects that Write-Host sends to it.
As you can see, Write-Ouput takes an array of PSObject as Input parameter and returns the same array as Output.
Out-String works fine with Write-Host because it is converting your array to a multiline string hence Write-Host will return it as literal to the Information Stream while without the Out-String it is taking all lines of your array and joining them into a single line string.
PS /> $test = #(
'this is a'
'test'
)
$writeHost = Write-Host -Object $test 6>&1
$writeOutput = Write-Output -Object $test
$writeHost.GetType()
$writeOutput.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False InformationRecord System.Object
True True Object[] System.Array
Edit
Following your comment, I don't know for sure why, I couldn't find any relevant information on MS Docs for the cmdlet but we can assume it is doing some sort of manipulation on the input objects. We do know that Write-Host is a wrapper for Write-Information.
Take this function as an example, it receives a [object] in this case it receives an array with 2 items as input and writes to the information stream a string representation of it's input:
$test = #(
'this is a'
'test'
)
function TestString ([object]$String) {
Write-Information ([string]$String) -InformationAction Continue
}
PS /> TestString $test
this is a test
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.
I'm trying to save values between instances of the script running.
I'm doing that by reading a text file at the beginning of the script, and then overwriting that file with the new values at the end of the script.
tracker.txt:
x=1
y=4
z=3
I'm reading the script using:
Get-Content "$Root\tracker.txt" | Foreach-Object{
$Position = $_.Split("=")
New-Variable -Name $Position[0] -Value $Position[1]
}
Unfortunately my $x $y and $z variables are being interpreted as a string instead of an integer.
Looking up New-Variable parameters, it doesn't seem like I can specify the value type.
Also I tried:
New-Variable -Name $Position[0] -Value [int]$Position[1]
And:
New-Variable -Name $Position[0] -Value ($Position[1] + 0)
But both did not work as expected.
How can I set these variables as an integer? I'm trying to use them later in a loop and that keeps failing because the variable can't be a string.
In order to save values, consider using Powershell's own object serialization. That is, Export-Clixml and Import-Clixml cmdlets. When an object is serialized, its contents are written on a file. In addition to values, data types are there too.
Handling multiple variables is easier, if those are stored in a collection such as a hash table. Like so,
# Save some values in a hash table
$myKeys =#{ "a" = 1; "b" = 2 }
$myKeys["a"]
1
# Check variable a's type. Int32 is as expected
$myKeys["a"].gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
# Serialize the hash table
Export-Clixml -Path keys.xml -InputObject $myKeys
# Create a new hash table by deserializing
$newKeys = Import-Clixml .\keys.xml
# Check contents
$newkeys["a"]
1
# Is the new a also an int32? Yes, it is
$newkeys["a"].gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
The keys.xml is, as expected an XML file. Check out its contents with, say, Notepad to see how the objects are stored. When working with complex objects, -Depth switch needs to be used. Otherwise, serialization saves only two levels of nesting, and that'll break complex objects.
I will use this:
(Get-Content tracker.txt) | foreach {
invoke-expression "[int]`$$($_.split("=")[0])=$($_.split("=")[1])"
}
It will create three variables with type int32.
To answer your specific question, you can force it like this.
#'
var1=1
var2=2
'# | out-file "$Root\tracker.txt"
Get-Content "$Root\tracker.txt" | Foreach-Object{
$Position = $_.Split("=")
New-Variable -Name $Position[0] -Value ([int]$Position[1])
}
$var1.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
However I really like this approach.
Create a template
$template = #'
{name*:var1}={[int]value:1}
{name*:var2}={[int]value:2}
'#
Now use ConvertFrom-String and assign to $data
#'
test1=1
test2=2
test3=3
test4=4
test5=5
'# | ConvertFrom-String -TemplateContent $template -OutVariable data
Let's create our variables now. Very easy to read.
$data | foreach {New-Variable -Name $_.name -Value $_.value}
Get-Variable test*
Name Value
---- -----
test1 1
test2 2
test3 3
test4 4
test5 5
And most importantly, they are int.
$test1.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
Putting it all together and pulling from a file
$template = #'
{name*:var1}={[int]value:1}
{name*:var2}={[int]value:2}
'#
Get-Content "$Root\tracker.txt" -raw |
ConvertFrom-String -TemplateContent $template |
foreach {New-Variable -Name $_.name -Value $_.value}
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