Why do process blocks correctly return a hashtable, instead of an array of hashtables? - powershell

I am trying to sort some REST API responses and extract data from them, and found this helpful answer on how to end up with a hashtable at the end.
$res= (Invoke-RestMethod #commonParams -uri "foo" -Method GET -FollowRelLink) | ForEach-Object { $_ } | Select-Object -Property id, filename | ForEach-Object -begin {$h=#{}} -process {$h[$_.filename] = $_.id} -end {$h}
Great, so I thought I'd shorten it a bit by getting rid of the begin/process/end blocks like so:
$res = (Invoke-RestMethod #commonParams -uri "foo" -Method GET -FollowRelLink) | ForEach-Object { $_ } | Select-Object -Property id, filename | ForEach-Object { $h=#{}; $h[$_.filename] = $_.id; $h }
All good, I should now be able to reference each item by it's key value using square brackets, but that doesn't work, only referencing by dot notation.
So I went searching and found this answer, which turned out to be correct:
$res.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
But, even changing from the generic hashtable constructor to using the .Add() method didn't help:
$res = (Invoke-RestMethod #commonParams -uri "foo" -Method GET -FollowRelLink) | ForEach-Object { $_ } | Select-Object -Property id, filename | ForEach-Object { $h=#{}; $h.add($_.filename, $_.id); $h }
$res.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
I add the process blocks back in:
$res = (Invoke-RestMethod #commonParams -uri "$foo" -Method GET -FollowRelLink) | ForEach-Object { $_ } | Select-Object -Property id, filename | ForEach-Object -begin {$h=#{}} -process {$h[$_.filename] = $_.id} -end {$h}
$res.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Hashtable System.Object
And it's all working as expected.
So I'd like to know, if possible, what exactly is happening when I use the process blocks as opposed to just making 'fake' multi-line functions in the pipeline?
And if anyone cares, I actually ended up avoiding the above issue completely by using Group-Object from here.

ForEach-Object emulates how script blocks work in the pipeline. The reason why the first and last snippets work as expected, returning a hashtable is because -Begin and -End not because of -Process which executes by default and is a mandatory parameter. If you don't use -Begin you would simply be creating and outputting a new hashtable per input object.
Using ForEach-Object
0..10 | ForEach-Object -Begin {
"executes only once, before the first input object is processed"
} -Process {
"processes each input object: $_"
} -End {
"executes only once, after the last input object is processed"
}
Using a script block
0..10 | & {
begin {
"executes only once, before the first input object is processed"
}
process {
"processes each input object: $_"
}
end {
"executes only once, after the last input object is processed"
}
}
about_Functions_Advanced_Methods explain how these blocks work.
Also your code could be simplified to this:
$h = #{}
Invoke-RestMethod #commonParams -uri "foo" -Method GET -FollowRelLink |
Write-Output |
ForEach-Object { $h[$_.filename] = $_.id }
Or using Group-Object -AsHashTable as you have already found:
$h = Invoke-RestMethod #commonParams -uri "foo" -Method GET -FollowRelLink |
Write-Output |
Group-Object FileName -AsHashTable
Write-Output in this case may not be needed if the output from Invoke-RestMethod is already enumerated.

Related

Powershell - Skip/overwrite the data which is already fetched instead of appending

I am trying to fetch the data in a loop for multiple Build Definition IDs, for each time one Definition ID is called the set of data will be printed, for the next time it should only fetch the data for the Next Build Definition ID and print only that data, I mean it should overwrite the data in a separate file.
But in my case its appending in a same csv file
param(
[string] $url =
"https://dev.azure.com/tfs/Projects/<projectname",
[string] $PAT = "<PAT>"
)
$token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($PAT)"))
$header = #{authorization = "Basic $token"}
#Read XML File
[xml]$xmlfile = Get-Content -Path "File.xml"
$data = $xmlfile | Select-Object -
Property SkipUpdate, Version | Where-Object{ $_.'SkipUpdate' -eq 'False'}
#Read JSON Data for Starting Changeset and buildDefinitionId
$jsonData = Get-Content "project_configuration.json" | ConvertFrom-Json
$branchNames = $jsonData.projectConfig
#Comparing the data got from XML and JSON and get only the data that we need
$metadata = $branchNames | Select-Object -Property name | Where-Object {$data.Version -eq $_.name}
$metadata.name
#Get the BuildDefinition ID , Start Chnageset and Branch Name of the Version whose SkipUpdate flag is set to False
$alphadata = $branchNames | Where-Object {$data.Version -eq $_.name}
#$alphadata.buildDefinitionId
#$alphadata.startChangeset
#$alphadata.branch
$alphadata | Select-Object -Property buildDefinitionId, startChangeset, branch
$getEndChangesetCall="https://dev.azure.com/tfs/projects/<projectname>_apis/build/builds?api-version=2.0&statusFilter=completed&resultFilter=succeeded&definitions=<REPLACEBUILDDEFINITIONID>"
foreach($item in $alphadata)
{
$newCall = $getEndChangesetCall.Replace("<REPLACEBUILDDEFINITIONID>",
$item.buildDefinitionId)
Write-Host "Calling API on $newcall"
$changeset = Invoke-RestMethod -Uri $newcall -Method Get -ContentType "application/json" -Headers $header -Verbose
$changeset | ConvertTo-Json | Out-File "<path-to-a-file>\ChangesetReq\$($item.builddefinitionId).json"
$changeset.value.sourceVersion[0]
#Final API call
$APIUrl= 'https://dev.azure.com/tfs/Projects/<ProjectName>/_apis/tfvc/changesets?searchCriteria.fromId=<FROMCHANGESET>&searchCriteria.toId=<TOCHANGESET>&$orderby=id asc&searchCriteria.itemPath=<PATHFILTER>'
$replaceEntity = $APIUrl.Replace("<FROMCHANGESET>", $item.startChangeset).Replace("<TOCHANGESET>",$changeset.value.sourceVersion[0]).Replace("<PATHFILTER>",$item.branch)
Write-Host "Final API Called on : $replaceEntity"
$reportcall = Invoke-RestMethod -Uri $replaceEntity -Method Get -ContentType "application/json" -Headers $header -Verbose
$reportcall | ConvertTo-Json | Out-File "<Path-to-the-file>\ChangesetReq\$($item.startChangeset).json"
$reportcall.value | ConvertTo-Json | Out-Null
$reportcall.value | Foreach-Object {
$changesetTfs = $_.changesetId
$authorName = $_.author.displayName
$commentMade = $_.comment
$date = $_.createdDate
[array] $report += $_.report| ForEach-Object {
[pscustomobject] #{
'ChangesetId'= $changesetTfs
'Author'= $authorName
'Comments'= $commentMade
'Date Checked In' = $date
}
}
}
Write-Host ($report | Format-Table -Force | Out-String)
$report | Export-CSV -Path ".\$($item.startChangeset).csv" -NoTypeInformation
}
How to overwrite the data/fetch only one particular Definition ID data into a csv file.
Continuing from my comments:
If I understand your question correctly now, you can do this to first gather all information in a variable $report, then create several groups of that based on the ChangesetId and finally save all groups into several CSV files.
$report = $reportcall.value | Foreach-Object {
# output the object to be collected in variable $report
foreach ($item in $_.report) {
[pscustomobject] #{
'ChangesetId' = $item.changesetId
'Author' = $item.author.displayName
'Comments' = $item.comment
'Date Checked In' = $item.createdDate
}
}
}
# group the array on property ChangesetId and export the items for each ID separately
$report | Group-Object ChangesetId | ForEach-Object {
$_.Group | Export-CSV -Path ".\$($_.Name).csv" -NoTypeInformation
}

Where-Object not filtering

Why isn't where-object working in this case?
$controlFlowArgs = #{ waitForEnter = $false }
$controlFlowArgs | Format-Table
$controlFlowArgs | Format-List
$result = $controlFlowArgs | Where-Object -FilterScript { $_.Name -eq "waitForEnter" }
$result
Output
Name Value # Format-Table
---- -----
waitForEnter False
Name : waitForEnter # Format-List
Value : False
# Missing result would be here
$controlFlowArgs is a HashTable. You should probably think it differently.
$result = $controlFlowArgs | Where-Object { $_["waitForEnter"] }
would store $false in $result.
Else you can use the Hashtable directly:
if ($controlFlowArgs["waitForEnter"]) {
...
}
Where-object is working fine. In this example, only the 'joe' hashtable appears. Confusingly the format takes up two lines.
#{name='joe';address='here'},#{name='john';address='there'} | ? name -eq joe
Name Value
---- -----
name joe
address here
It's still considered one thing:
#{name='joe';address='here'},#{name='john';address='there'} | ? name -eq joe |
measure-object | % count
1
If you want the value itself, use foreach-object (or select-object -expandproperty):
#{name='joe';address='here'},#{name='john';address='there'} | ? name -eq joe |
% name
joe
Usually powershell works with pscustomobjects:
[pscustomobject]#{name='joe';address='here'},
[pscustomobject]#{name='john';address='there'} | ? name -eq joe
name address
---- -------
joe here

Powershell - Remove Blank line from hash property variable

I created a script to collect remote SQL servers
#### Get number of SQL servers
$sql_servers = #()
foreach ($server in $servers){
# Loop through each server and check if server has service "MSSQLSERVER"
Try{
$sql = get-service -computername $server.DNSHOstname -ErrorAction Stop | where {$_.Name -eq "MSSQLSERVER"} | select MachineName
$sql_servers += New-Object PSObject -Property #{
Machine = $sql.MachineName
}
}
catch [Exception]
{
if ($_.Exception.GetType().Name -like "*COMException*") {
Write-Verbose -Message ('{0} is unreachable' -f $server.DNSHOstname) -Verbose
}
else{
Write-Warning $Error[0]
}
}
}
I'm getting desired results but variable contains multiple empty lines:
$sql_servers
Machine
-------
SQL1
SQL2
SQL3
I tried following to remove those blank lines without success.
$sql_servers = $sql_servers | Where-Object {$_}
$sql_servers = $sql_servers | ? {$_ -ne ""}
How to remove empty (blank) lines from variable ?
EDIT:
I found a workaround by removing hashtable property, instead of
$sql_servers += New-Object PSObject -Property #{
Machine = $sql.MachineName
}
}
i just set $sql_servers += $sql and no empty lines, but i'm curious is it possible to remove empty line using hash table.
Thanks
First thing to point out is your variable $sql_servers does not contain hashtables, but rather PSCustomObjects with one property. For this specific scenario you could remove empty entries by adjusting your command to
$sql_servers = $sql_servers | Where-Object {$_.machinename}
If it were hashtables, you could use
$sql_servers = $sql_servers | Where-Object {$_.values}
Here is a simple demonstration of both.
PSObject
1..5 | % {
if($_ % 2 -eq 0)
{
$num = $_
}
else
{
$num = $null
}
[PSCustomObject]#{
MachineName = $num
}
} -ov sql_servers
MachineName
-----------
2
4
$sql_servers | ? {$_.machinename} -ov sql_servers
MachineName
-----------
2
4
Hashtable
1..5 | % {
if($_ % 2 -eq 0)
{
$num = $_
}
else
{
$num = $null
}
#{
MachineName = $num
}
} -ov sql_servers
Name Value
---- -----
MachineName
MachineName 2
MachineName
MachineName 4
MachineName
$sql_servers | ? {$_.values} -ov sql_servers
Name Value
---- -----
MachineName 2
MachineName 4

No error when selecting non-existing property

I want PowerShell to throw an error when trying to select non-existing properties, but instead I get empty column as output. Example:
$ErrorActionPreference=[System.Management.Automation.ActionPreference]::Stop;
Set-StrictMode -Version 'Latest'
Get-Process *ex* | Select-Object Id,ProcessName,xxx
Id ProcessName xxx
-- ----------- ---
9084 explorer
11404 procexp
I wrote a script that is importing multiple text files by Import-Csv, but headers in those file may change, and I'll end up with empty columns being loaded to the system.
EDIT:
This is how I'm checking if the headers match:
$csv = Import-Csv -Delimiter ';' -Path $file.FullName
$FileHeaders = #(($csv | Get-Member -MemberType NoteProperty).Name)
if (Compare-Object $ProperHeaders $FileHeaders) {'err'} else {'ok'}
I know that's the way PowerShell works, but Set-StrictMode documentation was indeed a little misleading for me, as #Matt mentioned. I just wish Select-Object had some kind of "-NoNewImplicitProps" or "-ReadOnlyPipeline" switch that would do the job for me :). Thanks for the answers.
You are actually using what some people would call a feature. That is a simpler rendition of using Add-Member on all the array members to add an empty column.
In the case of Import-CSV what you do in that case is check the property names before the Select where you call them.
$data = Import-csv C:\Temp\file.csv
$props = $data | Get-member -MemberType 'NoteProperty' | Select-Object -ExpandProperty Name
I can see the documentation be a little misleading when it says for Set-StrictMode:
Prohibits references to non-existent properties of an object.
But in this case you are not trying to get the property reference but using a function of the Select-Object cmdlet. The following would have generated an error though
PS C:\Users\mcameron> Set-StrictMode -Version 'Latest'
(Get-Process *ex*).Bagels
The property 'Bagels' cannot be found on this object. Verify that the property exists.
At line:2 char:1
+ (Get-Process *ex*).Bagels
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], PropertyNotFoundException
+ FullyQualifiedErrorId : PropertyNotFoundStrict
PowerShell expanding non-existing properties to $null behaves as designed. AFAICS the only thing you could do is to explicitly check if all properties exist:
$props = 'Id', 'ProcessName', 'xxx'
$p = Get-Process *ex*
$missing = $p | Get-Member -Type *property |
Select-Object -Expand Name |
Compare-Object -Reference $props |
Where-Object { $_.SideIndicator -eq '<=' } |
Select-Object -Expand InputObject
if ($missing) {
throw "missing property $missing."
} else {
$p | Select-Object $props
}
Of course you could wrap this in a custom function:
function Select-ObjectStrict {
[CmdletBinding()]
Param(
[Parameter(
Position=0,
Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true
)]$InputObject,
[Parameter(
Position=1,
Mandatory=$true
)][string[]]$Property
)
Process {
$missing = $InputObject | Get-Member -Type *property |
Select-Object -Expand Name |
Compare-Object -Reference $Property |
Where-Object { $_.SideIndicator -eq '<=' } |
Select-Object -Expand InputObject
if ($missing) {
throw "missing property $missing."
} else {
$InputObject | Select-Object $Property
}
}
}
so it could be used like this:
Get-Process *ex* | Select-ObjectStrict -Property 'Id', 'ProcessName', 'xxx'
Something like this...?
$props = 'Id','ProcessName','xxx'
$availableProps = Get-Process *ex*|Get-Member -MemberType Properties | Select -ExpandProperty Name
$missingProps = $props | Where-Object {-not ($availableProps -contains $_)}
if ($missingProps) {
Write-Error "invalid property(s) $missingProps"
throw { [System.Management.Automation.PropertyNotFoundException] }
}
Get-Process *ex* | Select-Object $props
If you want receive error when selecting non-existing property
Use:
Set-StrictMode -Version Latest<br>
$Global:ErrorActionPreference = 'Stop'<br>

Read Column data from CSV

I have a CSV file
Name,Age,Data
Test,22,Yes
Test2,23,No
Test3,43,Yes
How can I process this file using PowerShell, so that I can replicate this functionality:
foreach(var HeaderName in CSV.HeaderName)
{
//Sample value Name
foreach(var data in HeaderColumn.Data)
{
//Do Something with data
//Sample values as we loop through will be
//Test,Test2,Test3
}
}
Where CSV.HeaderName should be having the values Name, Age, Data
and HeaderColumn.Data will have the column data for Name, Age and Data as we process the headers.
The PowerShell equivalent of your pseudo code would be something like this:
$csv = Import-Csv 'C:\path\to\your.csv'
$headers = $csv | Get-Member -MemberType NoteProperty | select -Expand Name
foreach ($header in $headers) {
foreach ($data in $csv.$header) {
# do stuff with $data
}
}
For better answers take a step back and describe the actual problem you're trying to solve instead of what you perceive as the solution.
Demonstration:
PS C:\> $csvfile = 'C:\temp\test.csv'
PS C:\> Get-Content $csvfile
Name,Age,Data
Test,22,Yes
Test2,23,No
Test3,43,Yes
PS C:\> $csv = Import-Csv $csvfile
PS C:\> $csv | Format-Table -AutoSize
Name Age Data
---- --- ----
Test 22 Yes
Test2 23 No
Test3 43 Yes
PS C:\> $headers = $csv | Get-Member -MemberType NoteProperty | select -Expand Name
PS C:\> $headers
Age
Data
Name
PS C:\> foreach ($header in $headers) {
>> foreach ($data in $csv.$header) {
>> Write-Host $data
>> }
>> }
>>
22
23
43
Yes
No
Yes
Test
Test2
Test3