Parsing a file to create an array of lines - powershell

This seems so incredibly simple but I am missing something. I just need to add an array to array[0], array[1], etc.
I am taking a vcard file and trying to read all the lines of one vcard and put them in an array and then place that array in an array so array[0] will be vcard 1, array[1] will be the next, etc.
$c = Get-Content -Path C:\temp\Contacts_Backup.vcf
$counter=0
$contact=#()
$allcontacts=#()
Foreach ($line in $c){
$contact += $line
if ($line -eq 'END:VCARD'){
$allcontacts[$counter++] = $contact
$contact=#()
}
}
Result:
Unable to index into an object of type System.String.

tl;dr:
You cannot "grow" an array by assigning to a nonexistent index; if you start with #() - an empty array - you must use += to "append" elements (arrays are fixed-size collections, so what really happens is that a new array must be allocated every time that contains the old elements followed by the new one).
Using += is therefore inefficient in loops, and there are two alternatives:
Use a .NET extensible list type to build an array-like collection more efficiently.
Preferably - because it is both more convenient and faster - let PowerShell create the array for you, simply by capturing the output from a foreach loop in a variable
($array = #(foreach (...) { ... }))
Details below.
Your code indeed has a problem, though the symptom it would produce differs from what your question currently states; using a simplified example:
PS> $allcontacts=#(); $allcontacts[0] = 'one', 'two'
Index was outside the bounds of the array. # ERROR
...
That is, #() creates an empty array, which you cannot implicitly "extend" by accessing a non-existent index.
Using +=, as you do with your $contacts array, does work:
$allcontacts=#(); $allcontacts += , ('one', 'two')
Note the use of array-construction operator , to ensure that the RHS operand is added as a whole as a single new element; without it, multiple elements would be added, one for each element.
However, while "extending" an array with += works, in reality you're creating a new array behind the scenes every time, because arrays are by definition fixed-size collections.
With larger collections, this can become a performance issue, and it is better to use a list data type instead, such as [System.Collections.Generic.List[object]][1]:
$allcontacts = New-Object Collections.Generic.List[object]
$allcontacts.Add(('one', 'two'))
Note the need to enclose the array to add - as a single list element - in (...) so that the .Add() method recognizes it as a single argument.
Taking a step back: You can let PowerShell collect the $contact sub-arrays in the overall $allcontacts array by simply capturing the output from the entire foreach command:
$c = Get-Content -Path C:\temp\Contacts_Backup.vcf
$contact=#()
$allcontacts = #(foreach ($line in $c){
$contact += $line
if ($line -eq 'END:VCARD'){
# Output the $contact array as a *single* object,
# using ",", the array-construction operator
, $contact
# Reset for the next contact.
$contact=#()
}
})
$allcontacts will end up as a regular PowerShell array, typed [object[]].
Use of the array-subexpression operator (#(...)) is only necessary if you need to ensure that $allcontacts is an array even if the *.vcf file contains only one contact definition.
[1] A non-generic alternative is [System.Collections.ArrayList], but its downside is that its .Add() method returns a value, requiring you to suppress that value with, e.g., $null = $arrayList.Add(...) so as not to pollute PowerShell's output stream.

This should do exactly what you want:
Add-Type -AssemblyName System.Collections
[System.Collections.Generic.List[object]]$allContacts = #()
[System.Collections.Generic.List[string]]$contact = #()
$filePath = 'C:\temp\Contacts_Backup.vcf'
$endMarker = 'END:VCARD'
foreach($line in [System.IO.File]::ReadLines($filePath))
{
if( $line -eq $endMarker ) {
$allContacts.Add( $contact.ToArray() )
$contact.Clear()
}
else {
$contact.Add( $line )
}
}
# Ready. Show result.
foreach( $vcf in $allContacts ) {
"Contact: "
$vcf
}

Related

powershell: concatenate an extension to each element of an array [duplicate]

I have an array and when I try to append a string to it the array converts to a single string.
I have the following data in an array:
$Str
451 CAR,-3 ,7 ,10 ,0 ,3 , 20 ,Over: 41
452 DEN «,40.5,0,7,0,14, 21 ,  Cover: 4
And I want to append the week of the game in this instance like this:
$Str = "Week"+$Week+$Str
I get a single string:
Week16101,NYG,42.5 ,3 ,10 ,3 ,3 , 19 ,Over 43 102,PHI,- 1,14,7,0,3, 24 ,  Cover 4 103,
Of course I'd like the append to occur on each row.
Instead of a for loop you could also use the Foreach-Object cmdlet (if you prefer using the pipeline):
$str = "apple","lemon","toast"
$str = $str | ForEach-Object {"Week$_"}
Output:
Weekapple
Weeklemon
Weektoast
Another option for PowerShell v4+
$str = $str.ForEach({ "Week" + $Week + $_ })
Something like this will work for prepending/appending text to each line in an array.
Set array $str:
$str = "apple","lemon","toast"
$str
apple
lemon
toast
Prepend text now:
for ($i=0; $i -lt $Str.Count; $i++) {
$str[$i] = "yogurt" + $str[$i]
}
$str
yogurtapple
yogurtlemon
yogurttoast
This works for prepending/appending static text to each line. If you need to insert a changing variable this may require some modification. I would need to see more code in order to recommend something.
Another solution, which is fast and concise, albeit a bit obscure.
It uses the regex-based -replace operator with regex '^' which matches the position at the start of each input string and therefore effectively prepends the replacement string to each array element (analogously, you could use '$' to append):
# Sample array.
$array = 'one', 'two', 'three'
# Prepend 'Week ' to each element and create a new array.
$newArray = $array -replace '^', 'Week '
$newArray then contains 'Week one', 'Week two', 'Week three'
To show an equivalent foreach solution, which is syntactically simpler than a for solution (but, like the -replace solution above, invariably creates a new array):
[array] $newArray = foreach ($element in $array) { 'Week ' + $element }
Note: The [array] cast is needed to ensure that the result is always an array; without it, if the input array happens to contain just one element, PowerShell would assign the modified copy of that element as-is to $newArray; that is, no array would be created.
As for what you tried:
"Week"+$Week+$Str
Because the LHS of the + operation is a single string, simple string concatenation takes place, which means that the array in $str is stringified, which by default concatenates the (stringified) elements with a space character.
A simplified example:
PS> 'foo: ' + ('bar', 'baz')
foo: bar baz
Solution options:
For per-element operations on an array, you need one of the following:
A loop statement, such as foreach or for.
Michael Timmerman's answer shows a for solution, which - while syntactically more cumbersome than a foreach solution - has the advantage of updating the array in place.
A pipeline that performs per-element processing via the ForEach-Object cmdlet, as shown in Martin Brandl's answer.
An expression that uses the .ForEach() array method, as shown in Patrick Meinecke's answer.
An expression that uses an operator that accepts arrays as its LHS operand and then operates on each element, such as the -replace solution shown above.
Tradeoffs:
Speed:
An operator-based solution is fastest, followed by for / foreach, .ForEach(), and, the slowest option, ForEach-Object.
Memory use:
Only the for option with indexed access to the array elements allows in-place updating of the input array; all other methods create a new array.[1]
[1] Strictly speaking, what .ForEach() returns isn't a .NET array, but a collection of type [System.Collections.ObjectModel.Collection[psobject]], but the difference usually doesn't matter in PowerShell.

Convert all values of OrderedDictionary of the powershell to string line and print to the log file

I have some problems with converting values of the OrderedDictionary of the PowerShell to the string line. I have the following hash table [OrderedDictionary]:
I try outputting data of values to a string:
for ($i = 0; $i -lt $DataResult.Count; $i++) {
$DataResult["$i"].Values |
ForEach-Object {
Write-Output $_
}
}
But it doesn't work, could you help me
Given your code sample I think you have an array of [Ordered] dictionaries? If so you should be able to unroll the values quite easily:
#This is just demo data for my testing:
$Dictionaries = #(
[Ordered]#{
P1 = 'Something'
P2 = 'SomethingElse'
}
[Ordered]#{
P1 = 'Another'
P2 = 'AnotherAnother'
}
)
# Unroll the values:
$Values = $Dictionaries.Values
If you ignore the demo data it's really just 1 line. $Values would be of type [Object[]] the elements of which are in their original string type. If needed you can re-cast as a string array:
$Values = [String[]]$values
Or you can specify directly with the unrolling:
# Unroll the values:
$Values = [String[]]$Dictionaries.Values
Or Type constrain the variable:
# Unroll the values:
[String[]]$Values = $Dictionaries.Values
Note: This casting will convert element values to strings as well. I'm going on the basis that's desired.
I'd also point out you don't really need Write-Output anywhere. Firstly, that's already implicit in normal PowerShell operations. Secondly, you cannot pipe a traditional For loop (although you can assign its output to a variable). At any rate, if your intent is to simply continue feeding this down the pipeline, you could remove $Values from any of the above examples and PowerShell will implicitly and natively do just that.

Processing items within a powershell array - multiple items

I have been reading about arrays in powershell and hash tables, I know the basic workings of an array and how to use foreach loop to get items within the array, my challange here is slightly different. I would like to pay what I call a multi dimension array to a script, and process the items contained within the array.
What is my setup.
$x = (1,"Server1",3,1),(4,"Server2",6,2),(3,"Server3",4,3)
$k = 'serverid','servername','locationid','appid' # key names correspond to data positions in each array in $x
$h = #{}
For($i=0;$i -lt $x[0].length; $i++){
$x |
ForEach-Object{
[array]$h.($k[$i]) += [string]$_[$i]
}
}
What am i trying to achieve ?
I am trying to achieve the structure of a database table within powershell. So literally treating each array item as a row.
So for example
(1,"Server1",3,1),(4,"Server2",6,2),(3,"Server3",4,3)
could be thought of as a table like below
enter image description here
I then want to loop for each item in the array to get the values, similar to the below example
[0].serverid = 1, [0].servername = server1, [0].locationid = 3, [0].applicationID = 1
[1].serverid = 4, [1].servername = server2, [1].locationid = 6, [1].applicationID = 2
what have I done ?
$x = (1,"Server1",3,1),(4,"Server2",6,2),(3,"Server3",4,3)
$k = 'serverid','servername','locationid','appid' # key names correspond to data positions in each array in $x
$h = #{}
For($i=0;$i -lt $x[0].length; $i++){
$x |
ForEach-Object{
[array]$h.($k[$i]) += [string]$_[$i]
}
}
$x
for ($i = 0; $i -lt $x.Count; $i++)
{
$myserverid = $x[$i][0]
$myservername = $x[$i][1]
$mylocationid = $x[$i][2]
$myappid = $x[$i][3]
write-host $myserverid
}
The Issues
If I set the following $x = (1,"Server1",3,1), then the loop is somewhat incorrect which is why I think the approach is wrong (more than one item works i.e $x = (1,"Server1",3,1),(4,"Server2",6,2),(3,"Server3",4,3)). The loop only works if you have more than one item within the array, hence why I want to re-examine the way the loop works.
Thanks in advance
Your approach relies on a nested (jagged) array: That is, you have an array of subarrays, each of which represents the tuple of values you want to assign to properties.
If there's only one subarray, you must create the nested array explicitly, using the unary form of , the array constructor operator:
# Construct a 1-element array that contains the 4-element subarray.
$x = , (1,"Server1",3,1)
With two or more, subarrays, you implicitly get a nested array:
# Construct a 3-element array, each element of which contains a 4-element subarray.
$x = (1,"Server1",3,1), (4,"Server2",6,2), (3,"Server3",4,3)
Note that in PSv5+ you could use a custom class to solve your problem:
class Custom {
[int] $serverid; [string] $servername;[int] $locationid; [int] $appid
Custom($propValueArray) {
$this.serverid = $propValueArray[0]; $this.servername = $propValueArray[1]; $this.locationid = $propValueArray[2]; $this.appid = $propValueArray[3]
}
}
# Use an array cast to construct [Custom] instances.
# Note the need for (...) around the array, because casts have high
# precedence in PowerShell.
[Custom[]] ((1,"Server1",3,1), (4,"Server2",6,2), (3,"Server3",4,3))
This would allow for processing such as:
# Construct all objects
$objects = [Custom[]] ((1,"Server1",3,1), (4,"Server2",6,2), (3,"Server3",4,3))
# Process each object.
foreach ($object in $objects) {
($myserverid = $object.serverid) # assign a property to a var; `()` also outputs
# ...
}

How to find the positions of all instances of a string in a specific line of a txt file?

Say that I have a .txt file with lines of multiple dates/times:
5/5/2020 5:45:45 AM
5/10/2020 12:30:03 PM
And I want to find the position of all slashes in one line, then move on to the next.
So for the first line I would want it to return the value:
1 3
And for the second line I would want:
1 4
How would I go about doing this?
I currently have:
$firstslashpos = Get-Content .\Documents\LoggedDates.txt | ForEach-Object{
$_.IndexOf("/")}
But that gives me only the first "/" on each line, and gives me that result for all lines at once. I need it to loop where I can figure out the space between each "/" for each line.
Sorry if I worded this badly.
You can indeed use the String.IndexOf() method for this!
function Find-SubstringIndex
{
param(
[string]$InputString,
[string]$Substring
)
$indices = #()
# start at position zero
$offset = 0
# Keep calling IndexOf() to find the next occurrence of the substring
# stop when IndexOf() returns -1
while(($i = $InputString.IndexOf($Substring, $offset)) -ne -1){
# Keep track of the index at which the substring was found
$indices += $i
# Update the offset, we'll want to start searching for the next index _after_ this one
$offset = $i + $Substring.Length
}
}
Now you can do:
Get-Content listOfDates.txt |ForEach-Object {
$indices = Find-SubstringIndex -InputString $_ -Substring '/'
Write-Host "Found slash at indices: $($indices -join ',')"
}
An concise solution is to use [regex]::Matches(), which finds all matches of a given regular expression in a given string and returns a collection of match objects that also indicate the index (character position) of each match:
# Create a sample file.
#'
5/5/2020 5:45:45 AM
5/10/2020 12:30:03 PM
'# > sample.txt
Get-Content sample.txt | ForEach-Object {
# Get the indices of all '/' instances.
$indices = [regex]::Matches($_, '/').Index
# Output them as a list (string), separated with spaces.
"$indices"
}
The above yields:
1 3
1 4
Note:
Input lines that contain no / instances at all will result in empty lines.
If, rather than strings, you want to output the indices as arrays (collections), use
, [regex]::Matches($_, '/').Index as the only statement in the ForEach-Object script block; the unary form of ,, the array constructor operator ensures (by way of a transient aux. array) that the collection returned by the method call is output as a whole. If you omit the , , the indices are output one by one, resulting in a flat array when collected in a variable.

Powershell skip element in array, if it blank

I have a powershell script, where I receive names of elements as a variables from Jenkins:
$IISarray = #("$ENV:Cashier_NAME", "$ENV:Terminal_NAME", "$ENV:Content_Manager_NAME", "$ENV:Kiosk_BO_NAME")
foreach ($string in $IISarray){
"some code goes here"
}
Sometimes random elements can be blank. How can I add a check to see if the current element in array is blank, skip it and go to next element?
It's easiest to use -ne '' to created a filtered copy of the array that excludes empty entries, courtesy of the ability of many PowerShell operators to act as a filter with an array-valued LHS.
Note: I'm assuming you mean to filter out empty strings, not also blank (all-whitespace) ones, given that undefined environment variables expand to an empty string.
# Sample array with empty elements.
# Note: No need for #(...), unless there's just *one* element.
$IISarray = "foo", "", "bar", "baz", ""
# Note the `-ne ''`, which filters out empty elements.
foreach ($string in $IISarray -ne ''){
$string # echo
}
The above yields:
foo
bar
baz
soundstripe's answer offers a Where-Object solution, which potentially provides added flexibility via the ability to specify an arbitrary filter script block, but the use of a pipeline is a bit heavy-handed for this use case.
Fortunately, PSv4+ offers the .Where() collection method, which performs noticeably better.
Let me demonstrate it with a solution that also rules out blank (all-whitespace) elements:
# Note the all-whitespace element, which we want to ignore too.
PS> ("foo", " ", "bar", "baz", "").Where({ $_.Trim() })
foo
bar
baz
Similar to the Where-Object cmdlet, you pass a script block to the .Where() method, inside of which the automatic $_ variable represents the input element at hand.
The .Trim() method trims leading and trailing whitespace from a string and returns the result.
An all-whitespace string therefore results in the empty string.
In a Boolean context (as the .Where() method script block implicitly is), the empty string evaluates to $false, whereas any non-empty string is $true.
You can choose to be explicit, however ($_.Trim() -ne ''), or even use a .NET method ([string]::IsNullOrWhiteSpace($_)).
You can use Where-Object to filter out null or empty values. It is very commonly used, so ? is shorthand for Where-Object.
$IISarray = #("$ENV:Cashier_NAME", "$ENV:Terminal_NAME", "$ENV:Content_Manager_NAME", "$ENV:Kiosk_BO_NAME")
foreach ($string in ($IISarray | ? {$_})){
"some code goes here"
}
The $_ is an automatic variable representing each incoming object in the pipeline. Both $null and the empty string '' are falsy in Powershell, so only non-null values with length > 0 will be passed in to your for loop.
# you can skip the `#` and brackets as well as the quotation marks
$IISarray = $ENV:Cashier_NAME, $ENV:Terminal_NAME, $ENV:Content_Manager_NAME, $ENV:Kiosk_BO_NAME
foreach($String in $IISarray) {
# trim the strings and check the length
if($String.Trim().Length -gt 0) {
"some code goes here"
}
}