I'm trying to write a few scripts using calculated properties (love that feature), and I seem to have an issue when I assign variables in it, for example:
ls | select #{N='What';E={get-acl | Tee-Object -Variable something}},#{N="ok";E={$something}}
it seems that the output I'm getting is:
And I'm unsure if there's a way to save a variable so I can use it in a different calculated property or even just piped to the next command? ( I do know that you can use that variable in the same calculated property though.)
Thanks to anyone who helps.
If you need to construct many inter-related properties and Select-Object is too verbose or inefficient, the idiomatic approach is to pipe to ForEach-Object and construct the output object manually:
Get-ChildItem |ForEach-Object {
$acl = $_ |Get-Acl
# now we can just use this local variable directly
$isOK = Test-WhateverYouNeedToTest $acl
# construct new object
[pscustomobject]#{
FilePath = $_.FullName
ACL = $acl
RuleCount = $acl.Access.Count
IsOK = $isOK
}
}
I'm unsure if there's a way to save a variable so I can use it in a different calculated property
You can't "cross-reference" the resolved value of one calculated property expression from another in the same call, if that's what you mean.
Property expressions are executed in their own local scope, which is why variable assignments also don't persist.
You could (but probably shouldn't) assign to a variable in a parent scope:
ls | select Name,#{N='ACL';E={ ($global:something = $_ |Get-Acl) }},#{N='RuleCount';E={$something.Access.Count}}
I would strongly recommend against it - keeping calculated properties free of side effects will make your code easier to read and understand.
[...] or even just piped to the next command
Again, you could, (but probably shouldn't) assign to a variable in a parent scope and use that with the downstream command:
ls | select #{N='ACL';E={ ($global:something = $_ |Get-Acl) }} | select ACL,#{N='RuleCount';E={$something.Access.Count}}
To explain why this is a bad idea, let's take this example instead:
function TimesTen {
1..10 | select #{N='Base';E={$global:v10 = $_ * 10; $_}} | select Base,#{N='Tenfold';E={$v10}}
}
This might appear to work as intended:
PS ~> TimesTen
Base Tenfold
---- -------
1 10
2 20
3 30
4 40
5 50
6 60
7 70
8 80
9 90
10 100
But this only works as long as pipeline output from Select-Object isn't buffered:
PS ~> $PSDefaultParameterValues['Select-Object:OutBuffer'] = 4
PS ~> TimesTen
Base Tenfold
---- -------
1 50
2 50
3 50
4 50
5 50
6 100
7 100
8 100
9 100
10 100
Oops! Now you need to explicitly prevent that:
function TimesTen {
1..10 | select #{N='Base';E={$global:v10 = $_ * 10; $_}} -OutBuffer 0 | select Base,#{N='Tenfold';E={$v10}}
}
With ForEach-Object, you avoid all of this.
Related
This is probably a dumb question but I cant seem to figure it out. How do I add a header to already existing variable? I have a variable with bunch of strings in it and I am trying to make it so it has a header which will simplify the script later on. Just as an example
$test = 1,2,3,4,5,6
Which comes out to be:
PS C:\windows\system32> $test
1
2
3
4
5
6
Where as what I want it to do is:
PS C:\windows\system32> $test
Numbers
--------
1
2
3
4
5
6
Additionally when implementing for each loop is it possible to add a blank header like to existing variable (from which foreach loop is running) and fill it automatically? for example going from original variable:
Letters Value
------- -----
a 10
b 15
c 23
d 25
To after for each loop:
Letters Value Numbers
------- ----- ------
a 10 1
b 15 2
c 23 3
d 25 4
This is a super generic example but basically i have one object with headers and when using a function someone made I am trying to populate the table with output of that function, the issue is that its returning stuff with no header and just returns the output only so I cant even make a hash table.
Thanks in advance.
In your example, your variable is a list of integers.
That's why there's no header.
If your variable were something else, like, a custom object, it would be displayed with headers.
To make your example a list of custom objects:
$test = 1..6
$test | Foreach-Object { [PSCustomObject]#{Number=$_} }
You can save this back to a variable:
$test = 1..6
$testObjects = $test | Foreach-Object { [PSCustomObject]#{Number=$_} }
If an object has four or fewer properties, it will be displayed as a table.
So you could also, say, make an object with two properties and still get headers.
$test = 1..6
$test | Foreach-Object { [PSCustomObject]#{Number=$_;NumberTimesTwo = $_ * 2} }
If you want to control how any object displays in PowerShell, you'll want to learn about writing formatters. There's a module I make called EZOut that makes these a lot easier to work with.
To offer an alternative to Start-Automating's helpful answer:
You can use Select-Object with calculated properties:
To turn a list of numbers into objects ([pscustomobject] instances) with a .Number property, whose display formatting defaults to the tabular display you're looking for:
$objects =
1,2,3,4,5,6 | Select-Object #{ Name='Number'; Expression={ $_ } }
Outputting $objects yields:
Number
------
1
2
3
4
5
6
You can use the same technique for adding additional properties to (copies of) existing objects, filling them at the same time (builds on the $objects variable filled above):
# Values for the new property.
$nums = 6..1 # same as: 6, 5, 4, 3, 2, 1
$i = #{ val = 0 } # Helper hashtable for indexing into $nums
$objects | Select-Object *, #{ Name='NewProperty'; Expression={ $nums[$i.val++] } }
Output:
Numbers NewProperty
------- -----------
1 6
2 5
3 4
4 3
5 2
6 1
If you want to add a property with $null values first and fill them later, use:
$objects | Select-Object *, NewProperty
Need to the limit the results of this to the first 20.
Is there another way I can loop this to get 20 results at a time?
$Array = #()
#Fill $Array with Data
$List = $Array | ForEach-Object {"$_`n"}
Personally, I would limit ther returned results prior to the looping construct. My example uses the names of service objects, but I needed something to use...
$TOlist = (Get-Service).Name
$TOlist | Select-Object -First 20 | ForEach-Object {
$_
}
You'll need to use at least two lines here. As far as I'm aware, you can't both assign your $TOlist variable and start sending each value down the pipeline.
I have a system that currently reads data from a CSV file produced by a separate system that is going to be replaced.
The imported CSV file looks like this
PS> Import-Csv .\SalesValues.csv
Sale Values AA BB
----------- -- --
10 6 5
5 3 4
3 1 9
To replace this process I hope to produce an object that looks identical to the CSV above, but I do not want to continue to use a CSV file.
I already have a script that reads data in from our database and extracts the data that I need to use. I'll not detail the fairly long script that preceeds this point but in effect it looks like this:
$SQLData = Custom-SQLFunction "SELECT * FROM SALES_DATA WHERE LIST_ID = $LISTID"
$SQLData will contain ~5000+ DataRow objects that I need to query.
One of those DataRow object looks something like this:
lead_id : 123456789
entry_date : 26/10/2018 16:51:16
modify_date : 01/11/2018 01:00:02
status : WRONG
user : mrexample
vendor_lead_code : TH1S15L0NGC0D3
source_id : A543212
list_id : 333004
list_name : AA Some Text
gmt_offset_now : 0.00
SaleValue : 10
list_name is going to be prefixed with AA or BB.
SaleValue can be any integer 3 and up, however realistically extremely unlikely to be higher than 100 (as this is a monthly donation) and will be one of 3,5,10 in the vast majority of occurrences.
I already have script that takes the content of list_name, creates and populates the data I need to use into two separate psobjects ($AASalesValues and $BBSalesValues) that collates the total numbers of 'SaleValue' across the data set.
Because I cannot reliably anticipate the value of any SaleValue I have to dynamically create the psobjects properties like this
foreach ($record in $SQLData) {
if ($record.list_name -match "BB") {
if ($record.SaleValue -gt 0) {
if ($BBSalesValues | Get-Member -Name $($record.SaleValue) -MemberType Properties) {
$BBSalesValues.$($record.SaleValue) = $BBSalesValues.$($record.SaleValue)+1
} else {
$BBSalesValues | Add-Member -Name $($record.SaleValue) -MemberType NoteProperty -Value 1
}
}
}
}
The two resultant objects look like this:
PS> $AASalesValues
10 5 3 50
-- - - --
17 14 3 1
PS> $BBSalesvalues
3 10 5 4
- -- - -
36 12 11 1
I now have the data that I need, however I need to format it in a way that replicates the format of the CSV so I can pass it directly to another existing powershell script that is configured to expect the data in the format that the CSV is in, but I do not want to write the data to a file.
I'd prefer to pass this directly to the next part of the script.
Ultimately what I want to do is to produce a new object/some output that looks like the output from Import-Csv command at the top of this post.
I'd like a new object, say $OverallSalesValues, to look like this:
PS>$overallSalesValues
Sale Values AA BB
50 1 0
10 17 12
5 14 11
4 0 1
3 3 36
In the above example the values from $AASalesValues is listed under the AA column, the values from $BBSalesValues is listed under the BB column, with the rows matching the headers of the two original objects.
I did try this with hashtables but I was unable to work out how to both create them from dynamic values and format them to how I needed them to look.
Finally got there.
$TotalList = #()
foreach($n in 3..200){
if($AASalesValues.$n -or $BBSalesValues.$n){
$AACount = $AASalesValues.$n
$BBcount = $BBSalesValues.$n
$values = [PSCustomObject]#{
'Sale Value'= $n
AA = $AACount
BB = $BBcount
}
$TotalList += $values
}
}
$TotalList
produces an output of
Sale Value AA BB
---------- -- --
3 3 36
4 2
5 14 11
10 18 12
50 1
Just need to add a bit to include '0' values instead of $null.
I'm going to assume that $record contains a list of the database results for either $AASalesValues or $BBSalesValues, not both, otherwise you'd need some kind of selector to avoid counting records of one group with the other group.
Group the records by their SaleValue property as LotPings suggested:
$BBSalesValues = $record | Group-Object SaleValue -NoElement
That will give you a list of the SaleValue values with their respective count.
PS> $BBSalesValues
Count Name
----- ----
36 3
12 10
11 5
1 4
You can then update your CSV data with these values like this:
$file = 'C:\path\to\data.csv'
# read CSV into a hashtable mapping the sale value to the complete record
# (so that we can lookup the record by sale value)
$csv = #{}
Import-Csv $file | ForEach-Object {
$csv[$_.'Sale Values'] = $_
}
# Add records for missing sale values
$($AASalesValues; $BBSalesValues) | Select-Object -Expand Name -Unique | ForEach-Object {
if (-not $csv.ContainsKey($_)) {
$csv[$_] = New-Object -Type PSObject -Property #{
'Sale Values' = $_
'AA' = 0
'BB' = 0
}
}
}
# update records with values from $AASalesValues
$AASalesValues | ForEach-Object {
[int]$csv[$_.Name].AA += $_.Count
}
# update records with values from $BBSalesValues
$BBSalesValues | ForEach-Object {
[int]$csv[$_.Name].BB += $_.Count
}
# write updated records back to file
$csv.Values | Export-Csv $file -NoType
Even with your updated question the approach would be pretty much the same, you'd just add another level of grouping for collecting the sales numbers:
$sales = #{}
$record | Group-Object {$_.list_name.Split()[0]} | ForEach-Object {
$sales[$_.Name] = $_.Group | Group-Object SaleValue -NoElement
}
and then adjust the merging to something like this:
$file = 'C:\path\to\data.csv'
# read CSV into a hashtable mapping the sale value to the complete record
# (so that we can lookup the record by sale value)
$csv = #{}
Import-Csv $file | ForEach-Object {
$csv[$_.'Sale Values'] = $_
}
# Add records for missing sale values
$sales.Values | Select-Object -Expand Name -Unique | ForEach-Object {
if (-not $csv.ContainsKey($_)) {
$prop = #{'Sale Values' = $_}
$sales.Keys | ForEach-Object {
$prop[$_] = 0
}
$csv[$_] = New-Object -Type PSObject -Property $prop
}
}
# update records with values from $sales
$sales.GetEnumerator() | ForEach-Object {
$name = $_.Key
$_.Value | ForEach-Object {
[int]$csv[$_.Name].$name += $_.Count
}
}
# write updated records back to file
$csv.Values | Export-Csv $file -NoType
I've been banging my head against the wall on this one.
I know that if I create an Array in Powershell, then copy the array, it'll copy it as a reference type not a value type.
So, the classic example is:
$c = (0,0,0)
$d = $c
$c[0] = 1
$d
1
0
0
The solution is to do $d = $c.clone()
This isn't working though if the array itself is a collection of reference types. This is my problem. I'm trying to create an array to track CPU usage by creating an array of Processes, wait a while, then check the latest values and calculate the differences. However the Get-Process creates a reference array. So when I do the following:
$a = ps | sort -desc id | where-object {$_.CPU -gt 20} #Get current values
$b = $a.clone() #Create a copy of those values.
sleep 20 #Wait a few seconds for general CPU usage...
$a = ps | sort -desc id | where-object {$_.CPU -gt 20} #Get latest values.
$a[0]
$b[0] #returns the same value as A.
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessNam
------- ------ ----- ----- ----- ------ -- ----------
3195 57 90336 100136 600 83.71 7244 OUTLOOK
$a and $b will always return the same value. If I try and do it one entry at a time using something like $b[0] = "$a[0].clone()" - PS complains that Clone can't be used in this case.
Any suggestions??
Also, just FYI, the second $a = PS |.... line isn't actually needed since $a is reference type to the PS list object, it actually gets updated and returns the most current values whenever $a is called. I included it to make it clearer what I'm trying to accomplish here.
To copy an array, you can do the following:
$c = (0,0,0)
$d = $c | foreach { $_ }
$c[0] = 1
"c is [$c]"
"d is [$d]"
Result
c is [1 0 0]
d is [0 0 0]
For your particular issue (comparing CPU usage of processes), something more specific would probably be better, as Keith pointed out.
Technically $d = $c is not any sort of array copy (of reference or value). It's just stashing a reference to the array $c refers to, into $d. I think you only need to grab the array of processes once and then call the Refresh method. You'll have to check the Exited property first to make sure the associated process is still running. Of course, this won't help you if you're interested in any new process that start up. In that case, grab a snapshot of processes at different times, weed out all but the intersection of processes between the two arrays (by process Id) and then compute the differences in their property values - again based on process Id. Take make this easier, you might want to put each snapshot in a hashtable keyed off the process Id.
I have a hashtable in PowerShell that looks like this:
$table = #{
1 = 3;
2 = 3;
5 = 6;
10 = 12;
30 = 3
}
I need to replace all "3" values with "4".
Is there a nice and clean way to do this without iterating over each pair and writing each one to a new hashtable?
Could the action with the same data be done easier if I'd use some other .NET collection class?
This throws exception that "Collection was modified":
$table.GetEnumerator() | ? {$_.Value -eq 3} | % { $table[$_.Key]=4 }
This adds another "Values" member to the object and breaks it:
$table.Values = $table.Values -replace 3,4
You can't modify the table while iterating over it, so do the iteration first and then do the updates. Just split your pipeline in two:
PS>$change = $table.GetEnumerator() | ? {$_.Value -eq 3}
PS>$change | % { $table[$_.Key]=4 }
PS>$table
Name Value
---- -----
30 4
10 12
5 6
2 4
1 4
The above answer didn't work for me and I couldn't fit this as a comment. The above single-lined answer didn't do anything. I am trying to change a single value to "Off" based on my hashtable.Key aka Name. Notice where I wrote $(backtick) is supposed to be a literal backtick, but it was messing up the code block. Here is my hashtable that is pulled from .\BeepVariables.txt.
Name Value
---- ----
$varAwareCaseSound "On"
$varEmailSound "On"
function SetHash2([string]$keyword, [string]$value){
$hash = #{}
$hash = (get-content .\BeepVariables.txt).replace(";"," $(backtick) n") | ConvertFrom-StringData
#($hash.GetEnumerator()) | ?{$_.key -like "*$keyword*"} | %{$hash[$_.value]=$value}
$hash
}
SetHash2 "aware" '"Off"'