Powershell: How to print list items in 1 string - arrays

Let's say i have this code:
$test = #("apple" , "orange", "pear")
If i do write-host $test[0] i will get apple returned.
If i do write-host $test[0] $test[1] $test[2] i will get this returned:
apple
orange
pear
When i try getting in 1 line as just a single string I am doing:
write-host "$test[0] $test[1] $test[2]"
But this returns:
apple orange pear[0] apple orange pear[1] apple orange pear[2]
It can't seem to recognise the index now that i Have added quotations
Question:
Anyone know how i can just get my result to look like:
"apple orange pear"
Note: i do need to wrap this in quotations as i will be using it a script elsewhere which would require this - this is just a simple example for what i need

In addition to Abdul's excellent suggestion of using -join, you can also use the $OFS (short for Output Field Separator) automatic variable to control how PowerShell expands arrays in string literals:
PS ~> $test = #("apple" , "orange", "pear")
PS ~> $OFS = ' '
PS ~> "$test"
apple orange pear
PS ~> $OFS = '-'
PS ~> "$test"
apple-orange-pear
PS ~> $OFS = '/'
PS ~> "$test"
apple/orange/pear
If left unset, a single space character will be used as the default separator:
PS ~> $OFS = $null
PS ~> "$test"
apple orange pear

A simple solution would be to join the array elements by a space.
Write-Host ($test -Join " ")

To complement the existing, helpful answers:
Write-Host is typically the wrong tool to use, unless the intent is to write to the display only, bypassing the success output stream and with it the ability to send output to other commands, capture it in a variable, or redirect it to a file. To output a value, use it by itself; e.g, $value, instead of Write-Host $value (or use Write-Output $value); see this answer. To explicitly print only to the display but with rich formatting, use Out-Host.
If you are using Write-Host deliberately, you can take advantage of the fact that it implicitly stringifies arrays by space-concatenating their elements; note that a space is invariably used in this case; that is, the $OFS preference variable does not apply:
$test = #("apple" , "orange", "pear")
Write-Host $test # -> 'apple orange pear'
If, by contrast, you want to output the string as data, just use an expandable (double-quoted) string ("..."), as explained in Mathias R. Jessen's helpful answer - as noted, you can use $OFS to change to a separator other than a space, but note that (a) $OFS is rarely used in practice and (b) it is important to restore it to its previous value after changing it, so as not to affect unrelated code.
# Note: The resulting string is *implicitly* output and prints
# to the display by default.
# The verbose equivalent would be
# Write-Output "$test"
"$test" # -> 'apple orange pear'
If you need more explicit - and side-effect-free - control over how an array is stringified, use the -join operator, as shown in Abdul Niyas P M's helpful answer.
Finally, as for what you tried:
write-host "$test[0] $test[1] $test[2]"
Embedding expressions such as $test[0] in "..." rather than _stand-alone variable references such as just $test requires use of $(...), the subexpression operator - without it, "$test[0]" expands $test as a whole - ironically resulting in the desired whole-array stringification you sought after all - and treats [0] as a verbatim part of the string.
This is a common pitfall; see this post for more information.
Therefore, what you needed was - note the $(...):
Write-Host "$($test[0]) $($test[1]) $($test[2])" # -> 'apple orange pear'

Related

Why does an array behave differently when directly assigned or retrieved from get-content

Here's something I don't understand.
When I define a variable:
$v = [byte]2, [byte]3
and check its type:
$v.getType().name
I get
Object[]
I then format $v:
'{0} {1}' -f $v
which prints
2 3
Now, if I get a file's first two bytes:
$f = (get-content 'xyz.txt' -encoding byte -readCount 2 -totalCount 2)
and check its type:
$f.getType().name
I get the same type as before: Object[].
However, unlike with $v, I cannot format $f:
'{0} {1}' -f $f
I get the error message Error formatting a string: Index (zero based) must be greater than or equal to zero and less than the size of the, although the length of the array is 2:
$f.length
returns
2
I don't understand why this is and would appreciate an explanation.
The behavior should be considered a bug in the -f operator; it is present as of v7.1 and reported in GitHub issue #14355; it does not affect other operators with array operands, such as -split or -in.
The workaround is to cast $f to [array] or, if creating a copy of the array is acceptable, #($f):
'abc' > xyz.txt
$f = get-content 'xyz.txt' -encoding byte -readCount 2 -totalCount 2
'{0} {1}' -f ([array] $f)
Note: Using #(), the array-subexpression operator - ... - #($f) - as Mathias R. Jessen notes - is the even simpler option, but do note that using #() involves cloning (creating a shallow copy of) the array, whereas the [array] cast in this case does not.
The alternative is to apply the [array] cast as a type constraint (by placing it to the left of the $f = ... assignment):
'abc' > xyz.txt
[array] $f = (get-content 'xyz.txt' -encoding byte -readCount 2 -totalCount 2)
'{0} {1}' -f $f
Note:
In PowerShell [Core] v6+, you must use -AsByteStream in lieu of -Encoding Byte.
The problem can also be avoided if -ReadCount 2 is omitted, but note that that decreases the performance of the command, because the bytes are then emitted one by one; that is, with -ReadCount 2 -TotalCount 2 a single object is emitted that is a 2-byte array as a whole, whereas just -TotalCount 2 emits the individual bytes, one by one to the pipeline, in which case it is then the PowerShell engine itself that collects these bytes in an [object[]] array for the assignment.
Note that applying #() directly to the command - #(get-content ...) - would not work in this case, because #(), due to parameter combination -ReadCount 2 -TotalCount 2, receives a single output object that happens to be an array as a whole and therefore wraps that single object in another array. This results in a single-element array whose element is the original 2-element array of bytes; for more information about how #(...) works, see this answer.
Background information:
The problem is an invisible [psobject] wrapper around each array returned by Get-Content -ReadCount (just one in this case), which unexpectedly causes the $f array passed to -f not to be recognized as such.
Note that PowerShell's other array-based operators, such as -in and -replace, are not affected.
The wrapper can be bypassed in two ways:
$f.psobject.BaseObject
casting to [array], as shown at the top.
Note:
Generally, output objects produced by cmdlets - as opposed to output produced by PowerShell code - have generally invisible [psobject] wrappers; mostly, they are benign, because PowerShell usually just cares about the .NET object being wrapped, not about the wrapper, but on occasion problems arise, such as in this case - see GitHub issue #5579 for a discussion of the problem and other contexts in which it manifests.
In order to test if a given object has a [psobject] wrapper, use -is [psobject]; e.g.:
$var = 1
$var -is [psobject] # -> $false
$var = Write-Output 1
$var -is [psobject] # -> $true, due to use of a cmdlet.
# You can also test command output directly.
(Write-Output 1) -is [psobject] # -> $true

Why does using a comma concatenate strings in PowerShell?

I have encountered a PowerShell behavior I don't understand. A comma between strings concatenates them and inserts a space in between, e.g.:
PS H:\> [string]$result = "a","b"
PS H:\> $result # a string with 3 characters
a b
If the result is interpeted as an array, then using a comma separates the elements of the array, e.g.:
PS H:\> [array]$result = "a","b"
PS H:\> $result # an array with 2 elements
a
b
I have tried searching for an explanation of this behavior, but I don't really understand what to search for. In the documentation about the comma operator, I see that it is used to initialize arrays, but I have not found an explanation for the "string concatenation" (which, I suspect, may not be the correct term to use here).
Indeed: , is the array constructor operator.
Since your variable is a scalar - a single [string] instance - PowerShell implicitly stringifies your array (converts it to a string).
PowerShell stringifies arrays by joining the (stringified) elements of the array with a space character as the separator by default.
You may set a different separator via the $OFS preference variable, but that is rarely done in practice.[1]
You can observe this behavior in the context of string interpolation:
PS> "$( "a","b" )"
a b
[1] As zett42 points out, an easier way to specify a different separator is to use the -join operator; e.g.,
"a", "b" -join '#' yields 'a#b'.
By contrast, setting $OFS without resetting it afterwards can cause later commands that expect it to be at its default to malfunction; with -join you get explicit control.

Why is PowerShell applying the predicate of a `Where` to an empty list

If I run this in PowerShell, I expect to see the output 0 (zero):
Set-StrictMode -Version Latest
$x = "[]" | ConvertFrom-Json | Where { $_.name -eq "Baz" }
Write-Host $x.Count
Instead, I get this error:
The property 'name' cannot be found on this object. Verify that the property exists and can be set.
At line:1 char:44
+ $x = "[]" | ConvertFrom-Json | Where { $_.name -eq "Baz" }
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : PropertyAssignmentException
If I put braces around "[]" | ConvertFrom-Json it becomes this:
$y = ("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }
Write-Host $y.Count
And then it "works".
What is wrong before introducing the parentheses?
To explain the quotes around "works" - setting strict mode Set-StrictMode -Version Latest indicates that I call .Count on a $null object. That is solved by wrapping in #():
$z = #(("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" })
Write-Host $z.Count
I find this quite dissatisfying, but it's an aside to the actual question.
Why is PowerShell applying the predicate of a Where to an empty list?
Because ConvertFrom-Json tells Where-Object to not attempt to enumerate its output.
Therefore, PowerShell attempts to access the name property on the empty array itself, much like if we were to do:
$emptyArray = New-Object object[] 0
$emptyArray.name
When you enclose ConvertFrom-Json in parentheses, powershell interprets it as a separate pipeline that executes and ends before any output can be sent to Where-Object, and Where-Object can therefore not know that ConvertFrom-Json wanted it to treat the array as such.
We can recreate this behavior in powershell by explicitly calling Write-Output with the -NoEnumerate switch parameter set:
# create a function that outputs an empty array with -NoEnumerate
function Convert-Stuff
{
Write-Output #() -NoEnumerate
}
# Invoke with `Where-Object` as the downstream cmdlet in its pipeline
Convert-Stuff | Where-Object {
# this fails
$_.nonexistingproperty = 'fail'
}
# Invoke in separate pipeline, pass result to `Where-Object` subsequently
$stuff = Convert-Stuff
$stuff | Where-Object {
# nothing happens
$_.nonexistingproperty = 'meh'
}
Write-Output -NoEnumerate internally calls Cmdlet.WriteObject(arg, false), which in turn causes the runtime to not enumerate the arg value during parameter binding against the downstream cmdlet (in your case Where-Object)
Why would this be desireable?
In the specific context of parsing JSON, this behavior might indeed be desirable:
$data = '[]', '[]', '[]', '[]' |ConvertFrom-Json
Should I not expect exactly 5 objects from ConvertFrom-Json now that I passed 5 valid JSON documents to it? :-)
With an empty array as direct pipeline input, nothing is sent through the pipeline, because the array is enumerated, and since there's nothing to enumerate - because an empty array has no elements - the Where (Where-Object) script block is never executed:
Set-StrictMode -Version Latest
# The empty array is enumerated, and since there's nothing to enumerate,
# the Where[-Object] script block is never invoked.
#() | Where { $_.name -eq "Baz" }
By contrast, in PowerShell versions up to v6.x "[]" | ConvertFrom-Json produces an empty array as a single output object rather than having its (nonexistent) elements enumerated, because ConvertFrom-Json in these versions doesn't enumerate the elements of arrays it outputs; it is the equivalent of:
Set-StrictMode -Version Latest
# Empty array is sent as a single object through the pipeline.
# The Where script block is invoked once and sees $_ as that empty array.
# Since strict mode is in effect and arrays have no .name property
# an error occurs.
Write-Output -NoEnumerate #() | Where { $_.name -eq "Baz" }
ConvertFrom-Json's behavior is surprising in the context of PowerShell - cmdlets generally enumerate multiple outputs - but is defensible in the context of JSON parsing; after all, information would be lost if ConvertFrom-Json enumerated the empty array, given that you wouldn't then be able to distinguish that from empty JSON input ("" | ConvertFrom-Json).
The consensus was that both use cases are legitimate and that users should have a choice between the two behaviors - enumeration or not - by way of a switch (see this GitHub issue for the associated discussion).
Therefore, starting with PowerShell [Core] 7.0:
Enumeration is now performed by default.
An opt-in to the old behavior is available via the new -NoEnumerate switch.
In PowerShell 6.x-, if enumeration is desired, the - obscure - workaround is to force enumeration by simply enclosing the ConvertFrom-Json call in (...), the grouping operator (which converts it to an expression, and expressions always enumerate a command's output when used in the pipeline):
# (...) around the ConvertFrom-Json call forces enumeration of its output.
# The empty array has nothing to enumerate, so the Where script block is never invoked.
("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }
As for what you tried: your attempt to access the .Count property and your use of #(...):
$y = ("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }
$y.Count # Fails with Set-StrictMode -Version 2 or higher
With the ConvertFrom-Json call wrapped in (...), your overall command returns "nothing": loosely speaking, $null, but, more accurately, an "array-valued null", which is the [System.Management.Automation.Internal.AutomationNull]::Value singleton that indicates the absence of output from a command. (In most contexts, the latter is treated the same as $null, though notably not when used as pipeline input.)
[System.Management.Automation.Internal.AutomationNull]::Value doesn't have a .Count property, which is why with Set-StrictMode -Version 2 or higher in effect, you'll get a The property 'count' cannot be found on this object. error.
By wrapping the entire pipeline in #(...), the array subexpression operator, you ensure treatment of the output as an array, which, with array-valued null output, creates an empty array - which does have a .Count property.
Note that you should be able to call .Count on $null and [System.Management.Automation.Internal.AutomationNull]::Value, given that PowerShell adds a .Count property to every object, if not already present - including to scalars, in a commendable effort to unify the handling of collections and scalars.
That is, with Set-StrictMode set to -Off (the default) or to -Version 1 the following does work and - sensibly - returns 0:
# With Set-StrictMode set to -Off (the default) or -Version 1:
# $null sensibly has a count of 0.
PS> $null.Count
0
# So does the "array-valued null", [System.Management.Automation.Internal.AutomationNull]::Value
# `. {}` is a simple way to produce it.
PS> (. {}).Count # `. {}` outputs
0
That the above currently doesn't work with Set-StrictMode -Version 2 or higher (as of PowerShell [Core] 7.0), should be considered a bug, as reported in this GitHub issue (by Jeffrey Snover, no less).

How does -join ";" work?

This answer includes a Powershell like of code that splits $env:path, applies a filter and puts the result together, to store it in $env:path.
$path = ($path.Split(';') | Where-Object { $_ -ne 'ValueToRemove' }) -join ';'
I was reading this code, and then, suddenly, a wild -join ';' appears. How does this work? What is the concept behind this? I would expect (<expression) would eventually become some object, but then this like reads <object> -join ';', so the join part would still be some independent token. How is it evaluated? I can see that it obviously does "the right thing", but how and why does it work? How exactly does Powershell evaluate this line? Looks like very dark magic to me.
As stated out by the docs the -Join Operator can be used mainly in two ways:
-Join <String[]>
<String[]> -Join <Delimiter>
Most of PowerShells operators work this way (and look similiar to method parameters). You got some values on the left side of an operator and execute an action (in this case joining) with the right-side-token, in your case the semicolon ;.
Take a look at the help (by typing Get-Help <section_name>) sections about_Join and about_Operators
Also a nice example copied from the docs (without the need of splitting beforehand) is:
PS> $a = "WIND", "S P", "ERSHELL"
PS> $a -join "OW"
WINDOWS POWERSHELL
To add to Clijsters' answer, it's an operator that acts on a string array (String[]). The character following the operator specifies the character to use to join each element of the array.
To break down the parts:
($path.Split(';') # take the *string* $path and split into an *array*.
# do this on the ; character
Where-Object { $_ -ne 'ValueToRemove' }) # exclude the array element which is equal to
# "ValueToRemove"
-join ';' # Join the *array* back into a *string*
# Put ";" between each array element.

How to split up a string by every new line and put each line into a separate array entry in PowerShell

My question is very simple and I am very new to PowerShell but I'm just wondering if there is a easy way to split a string by each line and add the contents of each line into a separate array entry.
To complement Don Cruickshank's helpful answer:
"`n" expands to a LF character only (Unix-style, Unicode code point U+000A), but your input may use CRLF sequences ("`r`n", Windows-style, Unicode code points U+000D and U+000A) as newlines (line breaks).
While a given platform's native newline character [sequence] is reflected in [Environment]::Newline, there's no guarantee that a given input (file)'s newlines uses it.
Notably, script-embedded here-strings (e.g., #"<newline>...<newline>"#) use whatever newline format the enclosing script file was saved with - and when reading scripts for execution, PowerShell accepts both LF-only and CRLF files on all platforms it supports.
Therefore, the most robust form to split a string into lines by newlines is the following idiom, which takes advantage of the fact that the -split operator by default accepts a regular expression as the split criterion:
$str -split '\r?\n' # returns array of lines contained in $str
The above handles input with both LF-only (\n) and CRLF (\r\n) newlines correctly,
because \r?\n matches each \n (LF) optionally (?) preceded by an \r (CR).
The help system for PowerShell contains lots of useful information (type help to get started.)
Use the -split operator to split a string. PowerShell uses the backtick character (`) for escape codes and so it would look like this:
$str -split "`n"
For example we can define a string and call Length on that string to get the Length (in characters):
PS C:\> $str = "One`nTwo`nThree"
PS C:\> $str
One
Two
Three
PS C:\> $str.Length
13
Now we'll create an array where that string is split into lines and get the length of the resulting array. Note that in PowerShell, arrays are shown one item per line and so it's appears just like the earlier result in this case!
PS C:\> $arr = $str -split "`n"
PS C:\> $arr
One
Two
Three
PS C:\> $arr.Length
3
PowerShell can be confusing at first. One trick is to convert data structures to JSON to see what's going on until you get used to it.
PS C:\> $str | ConvertTo-Json -Compress
"One\nTwo\nThree"
PS C:\> $arr | ConvertTo-Json -Compress
["One","Two","Three"]

Resources