Combine 2 PowerShell arrays - arrays

I'm trying to figure out how to combine 2 arrays as explained here by Microsoft.
$Source = 'S:\Test\Out_Test\Departments'
$Array = Get-ChildItem $Source -Recurse
$Array.FullName | Measure-Object # 85
$Array.FullName + $Source | Measure-Object # 86
$Source + $Array.FullName | Measure-Object # 1
The following only has 1 item to iterate through:
$i = 0
foreach ($t in ($Source + $Array.FullName)) {
$i++
"Count is $i"
$t
}
My problem is that if $Source or $Array is empty, it doesn't generate seperate objects anymore and sticks it all together as seen in the previous example. Is there a way to force it into separate objects and not into one concatenated one?

In PowerShell, the left-hand side operand determines the operator overload used, and the right-hand side gets converted to a type that satisfies the operation.
That's why you can observe that
[string] + [array of strings] results in a [string] (Count = 1)
[array of strings] + [string] results in an [array of strings] (Count = array size + 1)
You can force the + operator to perform array concatenation by using the array subexpression operator (#()) on the left-hand side argument:
$NewArray = #($Source) + $Array.FullName

This is because $Source is a System.String and + is probably an overloaded for Concat. If you cast $Source to an array, you will get your desired output:
[array]$Source + $Array.FullName | Measure-Object # 86

Related

Sort an array numerically by extracting integers in names

$Folders = Get-ChildItem -LiteralPath $PSScriptRoot | Where-Object {$_.PSIsContainer} | Select-Object -ExpandProperty BaseName
I get the output
Set 1
Set 10
Set 11 - A Memo
Set 2
Set 20
Set 22 - A Memo With Numbers 1234
Set 3
Set 33 - A Memo
...
$Folders = $Folders | Sort-Object {[INT]($_ -Replace 'Set ', '')} will sort the names in the right order but doesn't work if there is anything after the number like ' - A Memo'.
I've tried \b\d+\b on https://regexr.com but don't know how to implement that in this case.
I need a regex that can extract the number after 'Set ' and discard everything else.
RegEx is a whole other language in itself
Some alternatives for extracting the number, complementing g.sulman's excellent answer.
First the most simplest method, assuming "Set" and the number are always separated by space:
$Folders | Sort-Object { [int]($_ -split ' ')[1] }
This uses the -split operator to split the string on space character, which returns an array. Then it converts the 2nd element to int.
Use -match operator:
$Folders | Sort-Object { [int]( $_ -match '\d+' ? $matches[0] : 0 ) }
Note that conditional operator ? requires PS 7. Alternative for older PS versions:
$Folders | Sort-Object { [int]( if( $_ -match '\d+' ){ $matches[0] } else { 0 } ) }
The -match operator finds the first sub string that matches the RegEx \d+ which stands for one or more digits. The found sub string can be accessed through $matches[0].
Use Select-String cmdlet:
$Folders | Sort-Object { [int] ( $_ | Select-String -Pattern \d+ ).Matches[0].Value }
Same principle as the -match method. Just a different way to access the found sub string.
$names = #"
Set 1
Set 10
Set 11 - A Memo
Set 2
Set 20
Set 22 - A Memo With Numbers 1234
Set 3
Set 33 - A Memo
"# -split "`n"
$names | sort #{expression={[int]($_ -replace '^\w+\s|\s.+')}}
You can use an expression with Sort-Object. Above this is done to replace everything you don't care about and convert to int for number sorting (in text sorting 1, 10, 11, 2, 20 ... is expected.)
Regex breakdown
^ - start of the string
\w - word character (matches S)
+ - the previous thing as many times as need (matches Se, Set, Seet, Seeeeeeeet)
\s - space
| - or. so either everything before this, or everything after
\s - space
. - any character
+ - I think this one's covered above
Note: + matches 1 or more. Use * if you need to match 0 or more.
Edit: As per zett42's helpful comment, you could use [int]($_ -split ' ')[1] in the Sort-Object expression. This splits your name into an array, and takes the 2nd element of that array.

In PowerShell is there a way to return the index of an array with substring of a string

Is there another way return the indices of every instance of a substring in an array besides old fashioned looping through indices?
$myarray = #('herp','dederp','dedoo','flerp')
$substring = 'erp'
$indices = #()
for ($i=0; $i -lt $myarray.length; $i++) {
if ($myarray[$i] -match $substring){
$indices = $indices + $i
}
}
$thisiswrong = #($myarray.IndexOf($substring))
The conditional inside that kind of for loop is kinda cumbersome, and $thisiswrong only ever gets a value of [-1]
You can use LINQ (adapted from this C# answer):
$myarray = 'herp', 'dederp', 'dedoo', 'flerp'
$substring = 'erp'
[int[]] $indices = [Linq.Enumerable]::Range(0, $myarray.Count).
Where({ param($i) $myarray[$i] -match $substring })
$indices receives 0, 1, 3.
As for what you tried:
$thisiswrong = #($myarray.IndexOf($substring))
System.Array.IndexOf only ever finds one index and matches entire elements, literally and case-sensitively in the case of strings.
There's a more PowerShell-like, but much slower alternative, as hinted at by js2010's answer; you can take advantage of the fact that the match-information objects that Select-String outputs have a .LineNumber property, reflecting the 1-based index of the position in the input collection - even if the input doesn't come from a file:
$myarray = 'herp', 'dederp', 'dedoo', 'flerp'
$substring = 'erp'
[int[]] $indices =
($myarray | Select-String $substring).ForEach({ $_.LineNumber - 1 })
Note the need to subtract 1 from each .LineNumber value to get the 0-based array indices, and the use of the .ForEach() array method, which performs better than the ForEach-Object cmdlet.
If it was a file...
get-content file | select-string erp | select line, linenumber
Line LineNumber
---- ----------
herp 1
dederp 2
flerp 4

Why are parentheses needed for ConvertFrom-Json piped to Foreach-Object?

I have this function that gets the content of a json file.
I'm having some (to me) unexpected behavior trying to pipe this to ConvertTo-Json and Foreach-Object
Function GetConfigJson
{
$ConfigPath = "pathtomyjsonfile.json"
return Get-Content $ConfigPath | Out-String
}
The json is formatted like [{"key1":"value1"}, {"key2":"value2"}, ...]
To test the behavior I did the following:
$a = 0;
GetConfigJson | ConvertFrom-Json | ForEach-Object { $a++ };
$b = 0;
ConvertFrom-Json GetConfigJson | ForEach-Object { $b++ };
$c = 0;
ConvertFrom-Json (GetConfigJson) | ForEach-Object { $c++ };
$d = 0;
(ConvertFrom-Json (GetConfigJson)) | ForEach-Object { $d++ };
Write-Host "Test1: $a | Test2: $b | Test3: $c | Test4: $d";
Out of these only Test4 prints the expected number, Test1 and Test3 print 1 and Test2 gets an error: ConvertFrom-Json : Invalid JSON primitive: GetConfigJson.
Why do I need the parentheses around the ConvertFrom-Json for it to actually get piped as an array of objects?
(The parentheses around the function name GetConfigJson is more acceptable - but I still wonder why I need it there?)
It might help to take a look at the types of the output from each example - see below for a breakdown.
Helper Functions
I'm using these the two helper functions in the sections below. Note - I'm guessing your config has an array at the root as that seems to reproduce the issue, but feel free to update your question if that's not true.
function GetConfigJson
{
return "[{""name"":""first""}, {""name"":""second""}]"
}
function Write-Value
{
param( $Value )
write-host $Value.GetType().FullName
write-host (ConvertTo-Json $Value -Compress)
}
And then using your examples:
Example 1
# example 1a - original example
PS> $a = 0
PS> GetConfigJson | ConvertFrom-Json | ForEach-Object { $a++ };
PS> $a
1
# example 1b - show return types and values
PS> GetConfigJson | ConvertFrom-Json | foreach-object { Write-Value $_ }
System.Object[]
{"value":[{"name":"first"},{"name":"second"}],"Count":2}
ConvertFrom-Json returns an array object with two entries, but Foreach-Object only runs once because it iterates over the single array object, not the 2 items in the array.
Example 2
# example 2a - original example
PS> $b = 0;
PS> ConvertFrom-Json GetConfigJson | foreach-object { $b++ }
ConvertFrom-Json : Invalid JSON primitive: GetConfigJson.
At line:1 char:1
+ ConvertFrom-Json GetConfigJson | foreach-object { $b++ }
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [ConvertFrom-Json], ArgumentException
+ FullyQualifiedErrorId : System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand
# example 2b - show parameter types and values
PS> Write-Value GetConfigJson
System.String
"GetConfigJson"
ConvertFrom-Json throws an exception because PowerShell is treating GetConfigJson as a literal string, but "GetConfigJson" obviously isn't valid json, hence the exception.
Example 3
# example 3a - original example
PS> $c = 0;
PS> ConvertFrom-Json (GetConfigJson) | ForEach-Object { $c++ };
PS> $c
1
# example 3b - show parameter types and values
PS> ConvertFrom-Json (GetConfigJson) | ForEach-Object { Write-Value $_ };
System.Object[]
{"value":[{"name":"first"},{"name":"second"}],"Count":2}
This uses the Grouping Operator ( ... ) around GetConfigJson, so PowerShell evaluates GetConfigJson as a call to a function instead of taking it as a literal string. It first executes the GetConfigJson expression and then passes the result of that as a parameter into ConvertFrom-Json. However, it's still iterating over the single array object rather than over the items, so the foreach-object only runs once.
Example 4
# example 4a - original example
PS> $d = 0;
PS> (ConvertFrom-Json (GetConfigJson)) | ForEach-Object { $d++ };
PS> $d
2
# example 4b - show parameter types and values
PS> (ConvertFrom-Json (GetConfigJson)) | ForEach-Object { Write-Value $_ };
System.Management.Automation.PSCustomObject
{"name":"first"}
System.Management.Automation.PSCustomObject
{"name":"second"}
We're using the grouping operator twice here - once around GetConfigJson to evaluate that as an expression as opposed to a string, and once around the whole ConvertFrom-Json (GetConfigJson). The outer ( ... ) causes PowerShell to "unroll" the single array object and emits its items into the pipeline consumed by Foreach-object. This means ForEach-Object iterates over the items and we see two separate values written out by ``Write-Value```
Summary
You managed to hit a lot of PowerShell quirks with this question - hopefully this answer helps understand what they're all doing and why you see the behaviour you do.
Update for PowerShell 7
Per the comment below from #mklement0, the behaviour of ConvertFrom-Json changes from version 7 onwards - it doesn't enumerate arrays by default, and requires -NoEnumerate to opt out.
E.g., '[ 1, 2 ]' | ConvertFrom-Json | Measure-Object now reports 2 in v7+, whereas -NoEnumerate is required to get the v6- behaviour: '[ 1, 2 ]' | ConvertFrom-Json -NoEnumerate | Measure-Object (reports 1).

Powershell Get-Content + Array

how's it going?
I'm new on Powershell and I'm trying to simplify my code in order that I need to perform the same action in two files, the only thing that changes is the File Name and ReadCount size (15000 for the first file and 50000 for the second one).
When I run it the error shows:
Get-Content : An object at the specified path
C:\Folder\08_configuration_items 11_CI-Contract-new[0].csv does not
exist, or has been filtered by the -Include or -Exclude parameter. At
line:2 char:7
+ $i=0; Get-Content "C:\Folder\$fileArray[$len].csv" -ReadCount $sizeA ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (System.String[]:String[]) [Get-Content], Exception
+ FullyQualifiedErrorId : ItemNotFound,Microsoft.PowerShell.Commands.GetContentCommand
Get-Content : An object at the specified path
C:\Folder\08_configuration_items 11_CI-Contract-new[0]_1.csv does not
exist, or has been filtered by the -Include or -Exclude parameter. At
line:3 char:20
+ ... bookContent = Get-Content "C:\Folder\$fileArray[$len]_1.csv" | Selec ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (System.String[]:String[]) [Get-Content], Exception
+ FullyQualifiedErrorId : ItemNotFound,Microsoft.PowerShell.Commands.GetContentCommand
That's the code, not sure if I'm accessing the Array using the right way on Powershell.
$sizeArray = #(15000,50000)
$fileArray = #("08_configuration_items", "11_CI-Contract-new")
for($len=0; $len -le 1; $len++) {
$i=0; Get-Content "C:\Folder\$fileArray[$len].csv" -ReadCount $sizeArray[$len] | %{$i++; $_ | Out-File "C:\Folder\$fileArray[$len]_$i.csv" -Encoding "UTF8"}
$WorkbookContent = Get-Content "C:\Folder\$fileArray[$len]_1.csv" | Select -Index 0
for($j=2; $j -le $i; $j++) {
$CurrentFileContent = Get-Content "C:\Folder\$fileArray[$len]_$j.csv"
#($WorkbookContent, $CurrentFileContent) | Set-Content "C:\Folder\$fileArray[$len]_$j.csv"
}
}
Any ideias?
Thanks a lot
The problem here is with string interpolation. A variable name within a string will expand up until it reaches a special character in that name. Then it will append the remainder of the string and any interpolated strings afterwards. This commonly happens with the . character when accessing a property of an object within a string. A simple solution is to use the subexpression operator ($()).
Get-Content "C:\Folder\$($fileArray[$len]).csv"
An alternative is to build the path string another way and then pass it into the command. The method below uses the format operator (-f).
$Path = "C:\Folder\{0}.csv" -f $fileArray[$len]
Get-Content $Path
Your code with the subexpression operator added will look like the following:
$sizeArray = #(15000,50000)
$fileArray = #("08_configuration_items", "11_CI-Contract-new")
for($len=0; $len -le 1; $len++) {
$i=0; Get-Content "C:\Folder\$($fileArray[$len]).csv" -ReadCount $sizeArray[$len] | %{$i++; $_ | Out-File "C:\Folder\$($fileArray[$len])_$i.csv" -Encoding "UTF8"}
$WorkbookContent = Get-Content "C:\Folder\$($fileArray[$len])_1.csv" | Select -Index 0
for($j=2; $j -le $i; $j++) {
$CurrentFileContent = Get-Content "C:\Folder\$($fileArray[$len])_$j.csv"
#($WorkbookContent, $CurrentFileContent) | Set-Content "C:\Folder\$($fileArray[$len])_$j.csv"
}
}
You can see this behavior on a simpler scale using your $fileArray variable.
$filearray
08_configuration_items
11_CI-Contract-new
# Notice how the [0] gets appended to the string-cast $fileArray
"$filearray[0]"
08_configuration_items 11_CI-Contract-new[0]
$filearray[0]
08_configuration_items
"$($filearray[0])"
08_configuration_items
Since $fileArray is an array of strings, you have another unintended effect. With "$fileArray[0]", $fileArray will be interpolated and converted to a string output rather than an array. PowerShell by default will join array elements by a single space when casting as a string. So the resulting output format is arrayItem1 arrayItem2 arrayItem3[0]. [0] is not included as part of the variable evaluation.

How do I get an array of strings from a powershell pipeline?

I have a list of directory names that I want to convert to absolute paths, and strip out any invalid ones. My initial attempt at doing this was the pipeline
$dirs = 'dir1', 'dir2', 'dir3'
$paths = $dirs | % { Resolve-Path -ea 0 $_ } | Select -ExpandProperty Path
However, what I get back has type [Object[]] rather than [String[]]. I tried ensuring that the paths existed (by adding a ? { Test-Path $_ } step to the pipeline, but that didn't help.
What am I doing wrong? How do I get the directories as a list of strings? I need this so that I can concatenate the array to another array of strings, specifically
$newpath = (($env:PATH -split ';'), $paths) -join ';'
object[] is simply the default array in PowerShell, and it doesn't matter in this (and most) situations. You problem is that you're trying to join to arrays using (arr1, arr2). What this acutally does is create an array with two array objects, because , is an array construction operator. Try to join the arrays using +, like this:
$dirs = 'dir1', 'dir2', 'dir3'
$paths = $dirs | % { Resolve-Path -ea 0 $_ } | Select -ExpandProperty Path
$newpath = (($env:PATH -split ';') + $paths) -join ';'
and you could even skip the splitting, and just do
$dirs = 'dir1', 'dir2', 'dir3'
$paths = $dirs | % { Resolve-Path -ea 0 $_ } | Select -ExpandProperty Path
$newpath = "$env:PATH;$($paths -join ';')"
This seems to work:
$newpath = (#($env:PATH) + $paths) -join ';'
You don't really need to explicitly cast it as [string[]]. Powershell will figure that out from the command context and coerce the data to the proper type.

Resources