Why does PowerShell flatten arrays automatically? - arrays

I've written some pwsh code
"a:b;c:d;e:f".Split(";") | ForEach-Object { $_.Split(":") }
# => #(a, b, c, d, e, f)
but I want this
// in javascript
"a:b;c:d;e:f".split(";").map(str => str.split(":"))
[ [ 'a', 'b' ], [ 'c', 'd' ], [ 'e', 'f' ] ]
a nested array
#(
#(a, b),
#(c, d),
#(e, f),
)
Why? and what should I do

Use the unary form of ,, PowerShell's array-construction operator:
"a:b;c:d;e:f".Split(";") | ForEach-Object { , $_.Split(":") }
That way, the array returned by $_.Split(":") is effectively output as-is, as an array, instead of having its elements output one by one, which happens by default in a PowerShell pipeline.
, creates a - transient - wrapper array whose only element is the array you want to output. PowerShell then unwraps the wrapper array on output, passing the wrapped array through.

With foreach-object, it's usually useful to unwrap the array:
ps | foreach modules | sort -u modulename

You can alternatively create also a stack or a queue.
Below, I created with your array a stack.
$array = "a:b;c:d;e:f".Split(";")
$stack = New-Object -TypeName System.Collections.Stack
$array | ForEach-Object { $stack.Push($_.Split(":")) }
From here, the most used methods are .Push() to insert new items to your stack, .Peek() to use the first item of the stack and .Pop(), to retrieve and then remove the first item.
You mentioned that you wanted to create an array. This is also possible by using the ToArray() method.
$stackArray = $stack.ToArray()
$stackArray[2]
> a
> b
To keep in mind, creating a stack will inverse the order to the items.

Related

How to add a String to duplicate element in Array in Powershell

I have following array:
$array = 'A', 'B', 'C', 'A'
I want to identify the duplicate and add to the duplicate a string, for example, $string='Part 2'
so that I have
$array = 'A', 'B', 'C', 'A Part 2'
How do I do this in PowerShell?
Use a hashtable to keep track of strings you've already seen, then loop through all items in the array - if we've already seen the same, modify the item, otherwise leave it as is:
$array = 'A','B','C','A'
# Create hashtable to keep track of strings we've already encountered
$stringsSeen = #{}
# Now iterate over the array
$modifiedArray = $array |ForEach-Object {
if(!$stringsSeen.ContainsKey($_)){
# First encounter, add to hashtable
$stringsSeen[$_] = 1
}
else {
# We've seen it before! Time to update value and modify `$_`
$number = ++$stringsSeen[$_]
$_ += " Part $number"
}
# finally output `$_`, regardless of whether we modified it or not
$_
}
$modifiedArray now holds the new array of (partially) modified strings:
PS ~> $modifiedArray
A
B
C
A Part 2
Mathias R. Jessen's helpful answer shows an effective, readable solution.
The following solution streamlines the approach to make it more concise (though thereby potentially more obscure) and more efficient:
The .ForEach() array method is used to process the input array, as a faster alternative to the ForEach-Object cmdlet. (A foreach statement would be even faster, but is a bit more verbose).
It relies on the (non-obvious) fact that ++ applied to a non-existent hashtable entry implicitly creates the entry with value 1.
$array = 'A', 'B', 'C', 'A'
$ht = #{}
$newArray = $array.ForEach(
{ if (($count = ++$ht[$_]) -eq 1) { $_ } else { "$_ Part $count" } }
)
Note: $newArray is technically not an array, but of type [System.Collections.ObjectModel.Collection[psobject]], but that usually won't make a difference.
In PowerShell (Core) 7+ an even more concise solution is possible, using ?:, the ternary conditional operator:
$array = 'A', 'B', 'C', 'A'
$ht = #{}
$newArray = $array.ForEach({ ($count = ++$ht[$_]) -eq 1 ? $_ : "$_ Part $count" })

How to enter press square brackets in PowerShell on JSON File

How can I query change value of the "type" string in my JSON file with PowerShell? I can't get to the "type" string.
JSON file
{
"name": "b",
"compatibilityLevel": 1400,
"model": {
"culture": "c",
"dataSources":[
{
"type": "structured"
}
]
}}
PowerShell
$pathToJson = "C:\Model.bim"
$a = Get-Content $pathToJson | ConvertFrom-Json
$a.'model.dataSources.type' = "c"
$a | ConvertTo-Json -Depth 10 | Set-Content $pathToJson
tl;dr
$a.model.dataSources[0].type = 'c'
Note the need to specify index [0], because $a.model.dataSources is an array.
AS for what you tried:
$a.'model.dataSources.type' = "c"
You cannot use a property path stored in a string ('...') to directly access a nested property, because PowerShell interprets 'model.dataSources.type' as the name of a single property.
See this answer for workarounds.
Even with that problem corrected, $a.model.dataSources.type = "c" does not work, because $a.model.dataSources returns an array of values, and you cannot directly set a property on that array's elements; instead you must explicitly target the array element of interest, as shown above ([0]).
Note you can get the array elements' .type values with $a.model.dataSources.type, via a PSv+ feature called member-access enumeration, but that doesn't work on setting - by design, to prevent possibly inadvertent updating of all array elements with the same value; see this answer.

Appending a string to each item of an array

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.

PowerShell array unshift

After reading this helpful article on the Windows PowerShell Blog, I realized I could "shift" off the first part of an array, but didn't know how to "unshift" or push elements onto the front of an array in PowerShell.
I am creating an array of hash objects, with the last read item pushed onto the array first. I'm wondering if there is a better way to accomplish this.
## Create a list of files for this collection, pushing item on top of all other items
if ($csvfiles[$collname]) {
$csvfiles[$collname] = #{ repdate = $date; fileobj = $csv }, $csvfiles[$collname] | %{$_}
}
else {
$csvfiles[$collname] = #{ repdate = $date; fileobj = $csv }
}
A couple of things to note:
I need to unroll the previous array element with a foreach loop %{$_}, because merely referencing the array would create a nested array structure. I have to have all the elements at the same level.
I need to differentiate between an empty array and one that contains elements. If I attempt to unroll an empty array, it places a $null element at the end of the array.
Thoughts?
PS: The reason the empty hash element produces a NULL value is that $null is treated as a scalar in PowerShell. For details, see https://connect.microsoft.com/PowerShell/feedback/details/281908/foreach-should-not-execute-the-loop-body-for-a-scalar-value-of-null.
ANSWER:
Looks like the best solution is to pre-create the empty array when necessary, rather than code around the $null issue. Here's the rewrite using a .NET ArrayList and a native PoSh array.
if (!$csvfiles.ContainsKey($collname)) {
$csvfiles[$collname] = [System.Collections.ArrayList]#()
}
$csvfiles[$collname].insert(0, #{ repdate = $repdate; fileobj = $csv })
## NATIVE POSH SOLUTION...
if (!$csvfiles.ContainsKey($collname)) {
$csvfiles[$collname] = #()
}
$csvfiles[$collname] = #{ repdate = $repdate; fileobj = $csv }, `
$csvfiles[$collname] | %{$_}
You might want to use ArrayList objects instead, as they support insertions at arbitrary locations. For example:
# C:\> $a = [System.Collections.ArrayList]#(1,2,3,4)
# C:\> $a.insert(0,6)
# C:\> $a
6
1
2
3
4
You can simply use a plus operator:
$array = #('bar', 'baz')
$array = #('foo') + $array
Note: this re-creates actually creates a new array instead of changing the existing one (but the $head, $tail = $array way of shifting you refer to works extactly in the same way).

How do I have an array parameter that takes input from the args or the pipeline in Powershell?

I'm trying to write a Powershell function that takes an array argument. I want it to be called with the array either as an argument, or as pipeline input. So, calling looks something like this:
my-function -arg 1,2,3,4
my-function 1,2,3,4
1,2,3,4 | my-function
It's easy enough to get the first two:
function my-function {
param([string[]]$arg)
$arg
}
For pipeline input, though, it's harder. It's easy to get the arguments one at a time in the process block, by using ValueFromPipeline, but that means that the $args variable is a single value with pipeline input, but an array if -args is used. I can use $input in the END block, but that doesn't get -args input at all, and using $args in an END block only gets the final item from a pipeline.
I suppose that I can do this by explicitly collecting the argument values from the pipeline using begin/process/end blocks, as follows:
function my-function {
param([Parameter(ValueFromPipeline=$true)][string[]]$args)
begin {
$a = #()
}
process {
$a += $args
}
end {
# Process array here
$a -join ':'
}
}
But that seems very messy. It also seems like a relatively common requirement to me, so I was expecting it to be easy to implement. Is there an easier way that I have missed? Or if not, is there a way to encapsulate the argument handling into a sub-function, so that I don't have to include all that in every function I want to work like this?
My concrete requirement is that I'm writing scripts that take SQL commands as input. Because SQL can be verbose, I want to allow for the possibility of piping in the command (maybe generated by another command, or from get-contents on a file) but also allow for an argument form, for a quick SELECT statement. So I get a series of strings from the pipeline, or as a parameter. If I get an array, I just want to join it with "`n" to make a single string - line by line processing is not appropriate.
I guess another question would be, is there a better design for my script that makes getting multi-line input like this cleaner?
Thanks - the trick is NOT to use ValueFromPipeline then...
The reason I was having so much trouble getting things to work the way I wanted was that in my test scripts, I was using $args as the name of my argument variable, forgetting that it is an automatic variable. So things were working very oddly...
PS> 1,2,3,4 | ./args
PS> get-content args.ps1
param([string[]]$args)
if ($null -eq $args) { $args = #($input) }
$args -join ':'
Doh :-)
Use the automatic variable $input.
If only pipeline input is expected then:
function my-function {
$arg = #($input)
$arg
}
But I often use this combined approach (a function that accepts input both as an argument or via pipeline):
function my-function {
param([string[]]$arg)
# if $arg is $null assume data are piped
if ($null -eq $arg) {
$arg = #($input)
}
$arg
}
# test
my-function 1,2,3,4
1,2,3,4 | my-function
Here's another example using Powershell 2.0+
This example is if the parameter is not required:
function my-function {
[cmdletbinding()]
Param(
[Parameter(ValueFromPipeline=$True)]
[string[]]$Names
)
End {
# Verify pipe by Counting input
$list = #($input)
$Names = if($list.Count) { $list }
elseif(!$Names) { #(<InsertDefaultValueHere>) }
else { #($Names) }
$Names -join ':'
}
}
There's one case where it would error out without the 'elseif'. If no value was supplied for Names, then $Names variable will not exist and there'd be problems. See this link for explanation.
If it is required, then it doesn't have to be as complicated.
function my-function {
[cmdletbinding()]
Param(
[Parameter(Mandatory=$true,ValueFromPipeline=$True)]
[string[]]$Names
)
End {
# Verify pipe by Counting input
$list = #($input)
if($list.Count) { $Names = $list }
$Names -join ':'
}
}
It works, exactly as expected and I now I always reference that link when writing my Piped Functions.
ValueFromPipeline
You should use the pipeline (ValueFromPipeline) as PowerShell is specially designed for it.
$args
First of all, there is no real difference between:
my-function -<ParamName> 1,2,3,4 and
my-function 1,2,3,4 (assuming that the parameter $ParamName is at the first position).
The point is that the parameter name $args is just an unfortunate choice as $args is an automatic variable and therefore shouldn't be used for a parameter name. Almost any other name (that is not in the automatic variables list) should do as in the example from Sean M., but instead you should implement your cmdlet assuming that it will be called from the middle of a pipeline (see: Strongly Encouraged Development Guidelines).
(And if you want to do this completely right, you should give a singular name, plural parameter names should be used only in those cases where the value of the parameter is always a multiple-element value.)
Middle
The supposed cmdlet in your question is not a very good example as it only cares about the input and has a single output therefore I have created another example:
Function Create-Object {
Param([Parameter(ValueFromPipeline=$true)][String[]]$Name)
Begin {
$Batch = 0
$Index = 0
}
Process {
$Batch++
$Name | ForEach {
$Index++
[PSCustomObject]#{'Name' = $_; 'Index' = $Index; 'Batch' = $Batch}
}
}
}
It basically creates custom objects out of a list of names ($Names = "Adam", "Ben", "Carry").
This happens when you supply the '$Names` via an argument:
Create-Object $Names
Name Index Batch
---- ----- -----
Adam 1 1
Ben 2 1
Carry 3 1
(It iterates through all the names in $Name parameter using the ForEach cmdlet.)
And this happens when you supply the $Names via the pipeline:
$Names | Create-Object
Name Index Batch
---- ----- -----
Adam 1 1
Ben 2 2
Carry 3 3
Note that the output is quiet similar (if it wasn't for the batch column, the output is in fact the same) but the objects are now created in 3 separate batches meaning that every item is iterated at the process method and the ForEach loop only iterates ones every batch because the $Name parameter contains an array with one single item each process iteration.
Use case
Imaging that the $Names come from a slow source (e.g. a different threat, or a remote database). In the case you using the pipeline for processing the $Names your cmdlet can start processing the $Names (and pass the new objects onto the next cmdlet) even if not all $Names are available yet. In comparison to providing the $Names via an argument were all the $Names will need to be collected first before your cmdlet will process them and pass the new objects onto the pipeline.
I think you can achieve this by using the input processing methods BEGIN, PROCESS, and END blocks. I just ran into this. Here is my console output just playing around with this and you can see by putting the body of the function in the PROCESS block it behaves the way you would expect
Not working
λ ~ function test-pipe {
>> param (
>> # Parameter help description
>> [Parameter(ValueFromPipeline=$true)]
>> [String[]]
>> $Texts
>> )
>> $Texts | % {Write-Host $_}
>> }
λ ~ "this", "that", "another"
this
that
another
λ ~ $s = #("this", "that", "another")
λ ~ $s
this
that
another
λ ~ $s | test-pipe
another
λ ~ test-pipe -Texts $s
this
that
another
Working
λ ~ function test-pipe {
>> param (
>> # Parameter help description
>> [Parameter(ValueFromPipeline=$true)]
>> [String[]]
>> $Texts
>> )
>> BEGIN {}
>> PROCESS {$Texts | % {Write-Host $_}}
>> END {}
>>
>> }
λ ~ $s | test-pipe
this
that
another
λ ~

Resources