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 (...) { ... } and if (...) { ... } 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.

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)"
  • 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.