Iterate through CSV and create an array - powershell

I am newbie to Powershell. Need a logic for CSV automation. I have a CSV log file contains large number of API calls.
I need to go row by row and segregate the data, output should be like below. Sum of calls count and average of response time to be updated.
I have written complicated If else conditions for different types of API calls and able to take the scenario name and other values from the csv. My pain starts here, struggling to come to conclusion to move forward. Can i create an array and store all the values then do all the calculation later or write the values in another csv then do all the calculation to find the Count and average response time?
If i choose array, scenario should not be duplicated. For me its really hard to take a decision without knowing the available cmdlets for array and CSV. Please throw some light..
Thanks in advance...

Here is an approach you can use a combination of c# available to Powershell (which can be MUCH more efficient handling larger files and data).
The first component is you need some consistent logic to isolate the API category you want each URL to be assigned. From your screenshots, sometimes it seems you use last segment of the URL but others it is some path in the middle of the resource.
Here is just a quick approach where you pass in an array of categories, and if it can be matched to URI in any way, then that category is used. Otherwise, the URI stands as its own category. Please replace with whatever logic you want here.
function Get-ApiCategory {
param([string[]] $Categories, [string] $Text)
foreach ($c in $Categories) {
if ($Text.IndexOf($c) -gt 0) {
return $c
}
}
return $Text # Not found
}
Then, here is a method that (1) reads the large CSV file row-by-row and uses basic parsing logic (since your source data seems simple enough) without loading the full file into memory, and then (2) exports a CSV file with summary data.
function Write-SummaryToFile {
param([string[]] $Categories, [string] $InputFile, [string] $Output)
# Parse the file line-by-line (optimize for memory)
$result = #{}
$lineNum = 0
Write-Host $InputFile
foreach ($line in [System.IO.File]::ReadLines($InputFile)) {
if ($lineNum++ -lt 1) { continue } # Skip header
$cols = $line.Split(',')
$category = Get-ApiCategory $Categories $cols[0]
$new = #{
Category = $category
Count = [int]$cols[1]
AvgResponse = [double]$cols[2]
}
if ($result.ContainsKey($category)) {
$weighted = $result[$category].AvgResponse * $result[$category].Count
$result[$category].Count += $new.Count
$result[$category].AvgResponse = ($weighted + $new.AvgResponse * $new.Count) / $result[$category].Count;
} else {
$result[$category] = $new
}
}
# Output to file
if (Test-Path $Output) { Remove-Item $Output }
try {
$stream = [System.IO.StreamWriter] $Output
$stream.WriteLine('Scenario,Count,Avg_Response_Time')
$result.Values | ForEach-Object { $stream.WriteLine([string]::Format("{0},{1},{2}", $_.Category, $_.Count, $_.AvgResponse.ToString("0.##"))) }
}
finally {
$stream.Dispose()
}
}
Then, you are able to call these methods in an example like this:
$categories = #('MoveRequestQueue', 'DeliveryDate')
Write-SummaryToFile $categories 'c:\dev\scratch\ps1\test.csv' 'C:\dev\scratch\ps1\Output.csv'

Related

Powershell pass complex object By Value, not By Reference

I am trying to process some data in an ordered dictionary, then add that to another ordered dictionary, and I can do that by reinitializing my temporary dictionary, like this...
$collection = [Collections.Specialized.OrderedDictionary]::new()
foreach ($id in 1..5) {
$tempCollection = [Collections.Specialized.OrderedDictionary]::new()
foreach ($char in [Char]'a'..[Char]'e') {
$letter = ([Char]$char).ToString()
if ($id % 2 -eq 0) {
$letter = $letter.ToUpper()
}
$int = [Int][Char]$letter
$tempCollection.Add($letter, $int)
}
$collection.Add($id, $tempCollection)
}
foreach ($id in $collection.Keys) {
Write-Host "$id"
foreach ($key in $collection.$id.Keys) {
Write-Host " $key : $($collection.$id.$key)"
}
}
However, I feel like reinitializing is a bit inefficient/inelegant, and I would rather just .Clear() that temporary variable. Which leads to this...
$collection = [Collections.Specialized.OrderedDictionary]::new()
$tempCollection = [Collections.Specialized.OrderedDictionary]::new()
foreach ($id in 1..5) {
foreach ($char in [Char]'a'..[Char]'e') {
$letter = ([Char]$char).ToString()
if ($id % 2 -eq 0) {
$letter = $letter.ToUpper()
}
$int = [Int][Char]$letter
$tempCollection.Add($letter, $int)
}
$collection.Add($id, $tempCollection)
$tempCollection.Clear()
}
foreach ($id in $collection.Keys) {
Write-Host "$id"
foreach ($key in $collection.$id.Keys) {
Write-Host " $key : $($collection.$id.$key)"
}
}
The problem is that while simple objects like string, int, char, etc are passed by value, all complex objects like a dictionary are passed by reference. So I pass the SAME dictionary in every iteration of $collection.Add($id, $tempCollection) and the final state of $tempCollection is cleared, so the result is 5 empty members of $collection.
I know I can force something that is normally passed By Value to be By Reference using [Ref] as outlined here. And [Ref] is just an accelerator for System.Management.Automation.PSReference. So what I need is a way to force an argument By Value, but neither [Val] nor [ByVal] works, and searching for System.Management.Automation.PSValue doesn't seem to return anything useful either. The PSReference doco linked above says
This class is used to describe both kinds of references:
a. reference to a value: _value will be holding the value being referenced.
b. reference to a variable: _value will be holding a PSVariable
instance for the variable to be referenced.
which makes me think I can get to the Value somehow, but for the life of me I can't grok HOW. Am I on the right track, and just missing something, or am I misunderstanding this documentation completely?
Cloning also seems like a potential solution, i.e. $collection.Add($id, $tempCollection.Clone()), but Ordered Dictionaries don't implement ICloneable. .CopyTo() also isn't an option, since it doesn't necessarily maintain the order of the elements. Nor does .AsReadOnly() since
The AsReadOnly method creates a read-only wrapper around the current
OrderedDictionary collection. Changes made to the OrderedDictionary
collection are reflected in the read-only copy. Nor does OrderedDictionary implement .copy() as PSObject does.
I also tried making a new variable, like this...
$newCollection = $tempCollection
$collection.Add($id, $newCollection)
$tempCollection.Clear()
And that doesn't work either. So it seems that complex objects by reference seems to apply to more than just passed arguments.
It seems almost like my Ordered Dictionary choice/need is the root of the problem, but it seems like needing a unconnected copy of an Ordered Dictionary would not be such an edge case that it isn't supported.

I am trying to convert the "Return First recurring character in a string problem" from python to powershell

I have completed coding this same problem in python and was trying to generate a similar logic or atleast achieve a similar result in powershell.
Python Code-
def FRC(str):
h = {}
for ch in str:
if ch in h:
return ch
else:
h[ch] = 0
return '\0'
print(FRC("abcdedcba"))
I have tried a few possible codes and was able to only enumerate the array of characters to count their occurrences. Thank you for any suggestions.
Update1 - The code I have worked on is as follows:
function get-duplicatechar {
[CmdletBinding()]
param (
[string]$teststring
)
$hash = #()
$teststring = $teststring.ToCharArray()
foreach ($letter in $teststring)
{
if($letter -contains $hash){
return $letter
}else {
$hash = $hash + $letter
}
return "\0"
}
}
get-duplicatechar("saahsahh")
You could use the (.Net) HashSet class for this, which Add method (besides adding the value,) returns true if the element is added to the HashSet<T> object and false if the element is already present.
function get-duplicatechar {
[CmdletBinding()]
param (
[string]$str
)
$h = [System.Collections.Generic.HashSet[char]]::new()
foreach ($ch in $str.ToCharArray()) {
if(!$h.add($ch)) { return $ch }
}
}
Here's a working version using your code as base:
function get-duplicatechar {
[CmdletBinding()]
param (
[string]$teststring
)
$hash = #{}
$CharArray = $teststring.ToCharArray()
foreach ($letter in $CharArray) {
if($letter -in $hash.Keys) {
$letter
break
}
else {
$hash[$letter] = $null
}
}
}
One problem is that you are strongly typing $teststring to be a string, so when you add a character array later PowerShell just converts it into a string and thus $teststring remains a string (try $teststring.GetType() after $teststring = $teststring.ToCharArray() to see this for yourself).
One way to solve this is to do what I did and use a different variable for the character array. You could also solve it by changing the variable to a character array directly by replacing [string]$teststring with [char[]]$teststring, that way any strings input to the function will be automatically cast as a character array.
The next mistake is using -contains where you need -in. The letter doesn't contain the array, you're looking for the letter in the array, just like you did in Python.
You can drop the return keyword entirely, PowerShell does not need it. Any output in your function will be output from the function automatically.
You also call your collection variable "hash", but you made an array. I changed it to be an actual hashtable, just like in your Python code. I also changed the way we add to it to more closely reflect what you did in Python. There are many ways to do this, this is just one. Notice we'll need to add ".Keys" in our if-statement as well so we check for keys matching our letter.
I think that's it, ask if anything is unclear.

Iterating the results returned from fetchall_arrayref

I have a sql wherein I am fetching few records, sorted by full name.
My requirement is to extract chunks of similar names and then do some operation on it.
Say, the sql returns some records containing names like [name1,name1,name2,name3,name3]
I need to split them to [name1,name1] , [name2] , [name3,name3]
I am able to do it, but I am not happy with my implementation as I have to call doSomethingWithNames()twice.
while (my $paRecs = $pStatementHandle->fetchall_arrayref({}, 3)){
foreach my $phRec(#{$paRecs}){
my $pCurrentName = $phRec->{'FULL_NAME'};
if ((scalar(#aParentRecords) eq 0) or ($aParentRecords[-1] eq $pCurrentName)){
push(#aParentRecords, $pCurrentName);
} else {
doSomethingWithNames(\#aParentRecords);
#aParentRecords= ();
push(#aParentRecords, $pCurrentName);
}
}
};
doSomethingWithNames(\#aParentRecords); # This should be inside while loop
I believe am running into this issue because while doesn't go into the loop for
the last iteration as fetch* returns undef.
Sounds basic PERL stuff, but tried many loop constructs with no luck.
Any pointer will be a great help
The trick is to postpone existing the loop by converting it into an infinite loop. This requires checking the loop-terminating condition (!$rows) twice, though.
my $rows = [];
my $prev_name = '';
my #group;
while (1) {
$rows = $sth->fetchall_arrayref({}, 3) if !#$rows;
if (!$rows || $rows->[0]->{FULL_NAME} ne $prev_name)
if (#group) {
do_something(\#group);
#group = ();
}
last if !$rows;
}
$prev_name = $rows->[0]->{FULL_NAME};
push #group, shift(#$rows);
}

How do I populate an unknown number of variables from user input dynamically in powershell?

I am trying to figure out how to populate an unknown number of variables based on user input (writing a script that obtains certificates from a CA, and sometimes these certificates contain more than one name (SANs) and it is impossible to know how many so this needs to be dynamic).
I know I start with setting up params like this:
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[string[]]$SANs
)
And then I need to somehow take those values and assign them to $san1, $san2, $san3 and so on.
Being new to programming, I am not even sure what to call this. Would you use a foreach loop to somehow populate these variables?
ForEach ($SAN in $SANs) {
what do I do here?
}
The end result is a need to populate a string with these variables like dns=$san1&dns=$san2&dns=$san3 etc...
Functions and scripts can take parameters. The parameter block in your example looked like...
function foo {
Param([string[]]$SANs)
}
That parameter, $SANs, is an array of strings. A single string would look like this...
$stuff = 'barf'
An array of strings looks like this...
$stuff = #('barf', 'toot', 'ruff', 'meow')
So far so good? If you need to get each of the things in the array, you'd use a loop...
foreach ($thing in $stuff) { write-output $thing }
...for example...
$san_declaration
foreach ($thing in $stuff) {
if ($san_declaration.length -eq 0) {
$san_declaration = "dns=${thing}"
} else {
$san_declaration += "&dns=${thing}"
}
}
Now, if you (not that you asked) happen to be calling Get-Certificate, just remember the SANs parameter is a string array. In that case, you'd just pass in the string array instead of creating the string like you were doing.
Get-Certificate -DnsName $stuff

Powershell: Turn period delimited string into object properties

I have a string that looks something like this:
$string = "property1.property2.property3"
And I have an object, we'll call $object. If I try to do $object.$string it doesn't interpret it that I want property3 of property2 of property1 of $object, it thinks I want $object."property1.property2.property3".
Obviously, using split('.') is where I need to be looking, but I don't know how to do it if I have an unknown amount of properties. I can't statically do:
$split = $string.split('.')
$object.$split[0].$split[1].$split[2]
That doesn't work because I don't know how many properties are going to be in the string. So how do I stitch it together off of n amounts of properties in the string?
A simple cheater way to do this would be to use Invoke-Expression. It will build the string and execute it in the same way as if you typed it yourself.
$string = "property1.property2.property3"
Invoke-Expression "`$object.$string"
You need to escape the first $ since we don't want that expanded at the same time as $string. Typical warning: Beware of malicious code execution when using Invoke-Expression since it can do anything you want it to.
In order to avoid this you would have to build a recursive function that would take the current position in the object and pass it the next breadcrumb.
Function Get-NestedObject{
param(
# The object we are going to return a propery from
$object,
# The property we are going to return
$property,
# The root object we are starting from.
$rootObject
)
# If the object passed is null then it means we are on the first pass so
# return the $property of the $rootObject.
if($object){
return $object.$property
} else {
return $rootObject.$property
}
}
# The property breadcrumbs
$string = '"Directory Mappings"."SSRS Reports"'
# sp
$delimetedString = $String.Split(".")
$nestedObject = $null
Foreach($breadCrumb in $delimetedString){
$nestedObject = Get-NestedObject $nestedObject $breadcrumb $settings
}
$nestedObject
There are some obvious places where that function could be hardened and documented better but that should give you an idea of what you could do.
What's the use case here? You can split the string as you've described. This will create an array, and you can count the number of elements in the array so that n is known.
$string = "property1.property2.property3"
$split = $string.split('.')
foreach($i in 0..($split.Count -1)){
Write-Host "Element $i is equal to $($split[$i])"
$myString += $split[$i]
}