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}
Related
I am refactoring some old(ish) code, and I have used two approaches for creating some arrays and ordered dictionaries, for example...
[Collections.Specialized.OrderedDictionary]#{}
and
New-Object Collections.Specialized.OrderedDictionary
I wonder which of these two approaches to instantiating arrays and hashtables/dictionaries is better? FWIW, I need code that that is version agnostic from PS2.0 thru current. Performance is a secondary concern.
FWIW, it looks like the former is MUCH better from a performance standpoint. I tried
Measure-Command {
foreach ($i in 1..10000) {
$array1 = [Collections.Specialized.OrderedDictionary]#{}
}
}
Measure-Command {
foreach ($i in 1..10000) {
$array2 = New-Object Collections.Specialized.OrderedDictionary
}
}
and got 34 milliseconds vs 278 milliseconds. Of course I am not creating 10K instances, nor is performance the main priority, not is 278 milliseconds poor performance even if that was a priority. But it sure shows there is a big difference in the process, even if the end result is actually the same.
When I do the following:
cls
write-host "array1:"
$array1 = [System.Collections.Specialized.OrderedDictionary]#{}
$array1.GetType()
(Measure-Command -Expression {1..10000 | ForEach-Object { $array1.Add($_,"") }}).Milliseconds
write-host
write-host
write-host "array2:"
$array2 = New-Object System.Collections.Specialized.OrderedDictionary
$array2.GetType()
(Measure-Command -Expression {1..10000 | ForEach-Object { $array2.Add($_,"") }}).Milliseconds
I get, e.g., the following, i.e., I think they are the same type so I wouldn't expect any difference, performance or otherwise:
array1:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True OrderedDictionary System.Object
492
array2:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True OrderedDictionary System.Object
500
BTW, as of PS 3.0, $array3=[ordered]#{} gives you the same type+++:
array3
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True OrderedDictionary System.Object
+++
About Hash Tables
Beginning in PowerShell 3.0, you can use the [ordered] attribute to
create an ordered dictionary
(System.Collections.Specialized.OrderedDictionary) in PowerShell.
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 am making a PSObject from a json file
bar.json
{
"derp": {
"buzz": 42
},
"woot": {
"toot": 9000
}
}
I can make a PSCustomObject form the json using ConvertFrom-Json
$foo = Get-Content .\bar.json -Raw |ConvertFrom-Json
$foo.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSCustomObject System.Object
However if I try and splat multiple json files, I get an array
$foo = Get-Content .\*.json -Raw |ConvertFrom-Json
$foo.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False Object[] System.Array
In order to iterate over $foo, I need 2 different code paths depending on the object type.
Can I get a single object from multiple json files?
If not, how would I compress an array of objects into a single object?
I've tried to make a new object $bar that contains all the array items of $foo
$bar = new-object psobject
$bar | add-member -name $foo[0].psobject.properties.name -value $foo[0].'derp' -memberType NoteProperty
Update
Per Walter Mitty's request. If I load a single file and run $foo[0]
$foo = Get-Content .\bar.json -Raw |ConvertFrom-Json
$foo[0].gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSCustomObject System.Object
$foo[0]
derp woot
------------ ------------
#{Provisioners=System.Object[]; OS=windows; Size=; V... #{Provisioners=System.Object[]; OS=windows; Size=; V...
Solution
I initially implemented AP's answer, but later refactored it to use mklement0 answer.
While $allObjects is an array, it still allows me to reference values by name which is what I was looking for
$allObjects = #(
Get-ChildItem '.\foo' -Filter *.json -Recurse | Get-Content -Raw | ConvertFrom-Json
)
# iterating over nested objects inside an array is hard.
# make it easier by moving all array objects into 1 parent object where
# the 'key' is the name (opposed to AP's answer where filename = key)
$newObject = New-Object PSObject
foreach ($i in $allObjects) {
$i.psobject.members | ?{$_.Membertype -eq 'noteproperty'} |%{$newObject | add-member $_.Name $_.Value}
}
If all you want is an Array of JSON objects which are parsed from the different files:
$final = #{}
# Loop Thru All JSON Files, and add to $Final[<name>] = <JSON>
ls .\*.json | % {
$o = cat $_ -Raw | ConvertFrom-Json
$final[$_.name] = [PsCustomObject]$o
}
$final = [PsCustomObject]$final
This will produce a Name -> Data map for all your JSON files as a nested PSObject
AP.'s helpful answer is an elegant solution for creating a single top-level object ([pscustomobject] instance) that:
houses all JSON-converted objects as properties named for their input filenames.
E.g., $final would contain a bar.json property whose value is the object equivalent of the JSON string from the question.
A caveat is that with duplicate filenames the last file processed would "win".
See bottom for a usage note re Unix-style aliases ls and cat.
To get an array of all converted-from-JSON objects, the following is sufficient:
$allObjects = #(Get-ChildItem -Filter *.json | Get-Content -Raw | ConvertFrom-Json)
Note that if you only need a single directory's JSON files,
$allObjects = #(Get-Content -Path *.json -Raw | ConvertFrom-Json) works too.
Note the use of #(...), the array subexpression operator, which ensures that what is returned is treated as an array, even if only a single object is returned.
By default, or when you use $(...), the regular subexpression operator, PowerShell unwraps any (potentially nested) single-item collection and returns only the item itself; see Get-Help about_Operators.
A note on the use of Unix-style aliases ls and cat for PowerShell's Get-ChildItem and Get-Content cmdlets, respectively:
Now that PowerShell is cross-platform, these aliases only exist in the Windows edition of PowerShell, whereas a decision was made to omit them from PowerShell Core on Unix platforms, so as not to shadow the standard Unix utilities of the same name.
It is better to get into the habit of using PowerShell's own aliases, which follow prescribed naming conventions and do not conflict with platform-specific utilities.
E.g., gci can be used for Get-ChildItem, and gc for Get-Content
For the naming conventions behind PowerShell's own aliases, see the documentation.
What is the proper way to create an array, hashtable and dictionary?
$array = [System.Collections.ArrayList]#()
$array.GetType() returns ArrayList, OK.
$hashtable = [System.Collections.Hashtable]
$hashtable.GetType() returns RuntimeType, Not OK.
$dictionary = ?
How to create a dictionary using this .NET way?
What is the difference between dictionary and hashtable? I am not sure when I should use one of them.
The proper way (i.e. the PowerShell way) is:
Array:
> $a = #()
> $a.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Hashtable / Dictionary:
> $h = #{}
> $h.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Hashtable System.Object
The above should suffice for most dictionary-like scenarios, but if you did explicitly want the type from Systems.Collections.Generic, you could initialise like:
> $d = New-Object 'system.collections.generic.dictionary[string,string]'
> $d.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Dictionary`2 System.Object
> $d["foo"] = "bar"
> $d | Format-Table -auto
Key Value
--- -----
foo bar
If you want to initialize an array you can use the following code:
$array = #() # empty array
$array2 = #('one', 'two', 'three') # array with 3 values
If you want to initialize hashtable use the following code:
$hashtable = #{} # empty hashtable
$hashtable2 = #{One='one'; Two='two';Three='three'} # hashtable with 3 values
Hashtable and dictionary in Powershell is pretty much the same, so I suggest using hashtable in almost all cases (unless you need to do something in .NET where Dictionary is required)
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!!