Powershell color formatting with format operator
I'm using a format operator inside a script with a following example:
$current = 1
$total = 1250
$userCN = "contoso.com/CONTOSO/Users/Adam Smith"
"{0, -35}: {1}" -f "SUCCESS: Updated user $current/$total", $userCN
This excpectedly shows the following output:
SUCCESS: Updated user 1/1250 : contoso.com/CONTOSO/Users/Adam Smith
The format operator is there to keep the targeted output text in place with current / total running numbers varying between 1-99999. Without the format operator I could highlight the success line like this:
Write-Host -BackgroundColor Black -ForegroundColor Green "SUCCESS: Updated user $current/$total: $userCN"
But the question is how could I use the highlight-colors combined with the format operator? There's only the -f
parameter and it doesn't allow the color parameters because, well, it's not the same thing as Write-Host
in the first place.
Solution 1:
Unlike other shells, PowerShell allows you to pass commands and expressions as command arguments simply by enclosing them in parentheses, i.e by using (...)
, the grouping operator.
When calling PowerShell commands (cmdlets, scripts, functions), the output is passed as-is as an argument, as its original output type.
Therefore, Theo's solution from a comment is correct:
Write-Host -BackgroundColor Black -ForegroundColor Green `
("{0, -35}: {1}" -f "SUCCESS: Updated user $current/$total", $userCN)
That is, the -f
expression inside (...)
is executed and its output - a single string in this case - is passed as a positional argument to Write-Host
(implicitly binds to the -Object
parameter).
Note that you do not need, $(...)
, the subexpression operator, in this case:
(...)
is sufficient to enclose an expression or command.
In fact, in certain cases $(...)
can inadvertently modify your argument, because it acts like the pipeline; that is, it enumerates and rebuilds array expressions as regular PowerShell arrays (potentially losing strong typing) and unwraps single-element arrays:
# Pass a single-element array to a script block (which acts like a function).
# (...) preserves the array as-is.
PS> & { param($array) $array.GetType().Name } -array ([array] 1)
Object[] # OK - single-element array was passed as-is
# $(...) unwraps it.
PS> & { param($array) $array.GetType().Name } -array $([array] 1)
Int32 # !! Single-element array was unwrapped.
# Strongly-typed array example:
PS> & { param($array) $array.GetType().Name } ([int[]] (1..10))
Int32[] # OK - strongly typed array was passed as-is.
# Strongly-typed array example:
PS> & { param($array) $array.GetType().Name } $([int[]] (1..10))
Object[] # !! Array was *enumerated and *rebuilt* as regular PowerShell array.
The primary use of $(...)
is:
-
expanding the output from expressions or commands inside expandable strings (string interpolation)
-
To send the output from compound statements such as
foreach (...) { ... }
andif (...) { ... }
or multiple statements directly through the pipeline, after collecting the output up front (which(...)
does as well); however, you can alternatively wrap such statements& { ... }
(or. { ... }
in order to execute directly in the caller's scope rather than a child scope) in order to get the usual streaming behavior (one-by-one passing of output) in the pipeline.- Taking a step back: Given that you already can use compound statements as expressions in variable assignments - e.g.,
$evenNums = foreach ($num in 1..3) { $num * 2 }
- and expressions generally are accepted as the first segment of a pipeline - e.g.,'hi' | Write-Host -Fore Yellow
- it is surprising that that currently doesn't work with compound statements; this GitHub issue asks if this limitation can be lifted.
- Taking a step back: Given that you already can use compound statements as expressions in variable assignments - e.g.,
In the context of passing arguments to commands:
-
Use
$(...)
, the subexpression operator only if you want to pass the output from multiple commands or (one or more) compound statements as an argument and/or, if the output happens to be a single object, you want that object to be used as-is or, if it happens to be a single-element array (enumerable), you want it to be unwrapped (pass the element itself, not the array.- Of course, if you're constructing a string argument,
$(...)
can be useful inside that string, for string interpolation - e.g.,Write-Host "1 + 1 = $(1 + 1)"
- Of course, if you're constructing a string argument,
-
Use
@(...)
, the array subexpression operator only if you want to pass the output from multiple commands as an argument and/or you want to ensure that the output becomes a array; that is, the output is returned as a (regular PowerShell) array, of type[object[]]
, even if it happens to comprise just one object. In a manner of speaking it is the inverse of$(...)
's behavior in the single-object output case: it ensures that single-object output too becomes an array.