Summary: Learn about using formatted strings in Windows PowerShell.
Microsoft Scripting Guy, Ed Wilson, here. Today we have another guest blog by June Blender. To read more of June’s previous posts, see these Hey, Scripting Guy Blog posts. Now, here’s June…
I recently had the opportunity to help the Windows Azure and ASP.NET product teams with some Windows PowerShell automation. Although I was supposed to be the helper, I probably learned more from the interaction than anyone else. I learned about Windows Azure and the Windows Azure module for Windows PowerShell. And I learned a whole bunch about Windows PowerShell. I'm convinced that helping always benefits the helper. You always learn by interacting. This case was no exception.
One of the most fun things I learned was about string formatting.
When I saw the first drafts of the scripts, they had a lot of Write-Verbose calls like this one:
$VerbosePreference = "Continue"
$filepath = "C:\ps-test\test.txt"
$owner = "juneb"
$result = $true
Write-Verbose ("Created {0} by {1}. Result is {2}." –f $filepath, $owner, $result)
Here's the result:
VERBOSE: Created C:\ps-test\test.txt by juneb. Result is True.
This statement uses string formatting, that is, it creates a little template called a "format string" in the quoted string:
"Created {0} by {1}. Result is {2}."
The placeholders in the template are integers enclosed in curly braces. The integers must begin with 0 and increment by 1.
{0}, {1}, {2} ...
You can put the placeholders anywhere in the format string and use a placeholder multiple times in the same string.
To assign values to the placeholders, type -f (for format), and then type a comma-separated list of the values in the order that you want them to appear. The first value replaces the first placeholder {0} in the string, the second values replaces the {1}, and so on.
Any valid expression can be a value.
PS C:\> "Created {0} on {1} by {2}. Result is {3} times {4}." –f $filepath, [datetime]"1/12/2014", $owner, "Success", 14/7
Created C:\ps-test\test.txt on 1/12/2014 12:00:00 AM by juneb. Result is Success times 2.
You can use string formatting anywhere in Windows PowerShell where a string value is permitted. When you use a formatted string as the value of the Message parameter of Write-Verbose, you must enclose it in parentheses. Otherwise, Windows PowerShell tries to interpret -f as a parameter of Write-Verbose.
PS C:\ps-test> Write-Verbose -Message "{0}" -f "Hi" -Verbose
Write-Verbose : A parameter cannot be found that matches parameter name 'f'.
At line:1 char:30
+ Write-Verbose -Message "{0}" -f "Hi" -Verbose
+ ~~
+ CategoryInfo : InvalidArgument: (:) [Write-Verbose], ParameterBindingException
+ FullyQualifiedErrorId : NamedParameterNotFound,Microsoft.PowerShell.Commands.WriteVerboseCommand
PS C:\ps-test> Write-Verbose -Message ("{0}" -f "Hi") -Verbose
VERBOSE: Hi
The critical element here is the cool -f, which is the Windows PowerShell format operator. It's described briefly in about_Operators, but the real info is on the following sites:
These docs are really worth reading. Someone put a lot of time into them and wrote extensive and very useful remarks. I've had them bookmarked for years.
To use the -f operator, you put the format string (with placeholders) on the left of the -f and the values on the right:
<format string> -f <values>
"{0} is the {1}." - f "ScriptingWife", "best"
String formatting is really familiar to people who code in languages that don't resolve expressions or variables that occur in strings. In these languages, you use a format string or you use the plus operator (+) to concatenate strings with expressions, like in this Python example:
>>> filepath = "C:\ps-test\file.txt"
>>> owner = "juneb"
>>> result = True
>>>
>>> "Created " + filepath + " by " + owner + ". Result is " + str(result) + "."
'Created C:\\ps-test\file.txt by juneb. Result is True.'
Good luck getting the spaces right on the first try! Of course, you can do this in Windows PowerShell, too. Fortunately, it's not required.
PS C:\> $filepath = "C:\ps-test\file.txt"
PS C:\> $owner = "juneb"
PS C:\> $result = $True
PS C:\> "Created " + $filepath + " by " + $owner + ". Result is " + $result + "."
Created C:\ps-test\file.txt by juneb. Result is True.
String formatting, -f, and string concatenation with the plus operator (+) work in Windows PowerShell, but I always thought that they were only included to help people who are familiar with other languages work successfully in Windows PowerShell.
This is fun stuff, I thought. But, honestly, there's no need to do this in Windows PowerShell, because you can include expressions right in a double-quoted string.
Note Single-quoted strings are different. Windows PowerShell prints them as-is with no substitutions.
PS C:\> 'Created $filepath by $owner. Result is $result.'
Created $filepath by $owner. Result is $result.
PS C:\> "Created $filepath by $owner. Result is $result."
Created C:\ps-test\file.txt by juneb. Result is True.
/NOTE]
So, when I encountered formatted strings in the Windows Azure module scripts, I replaced them with the "PowerShell way" to do it. For example, I changed:
$VerbosePreference = "Continue"
$filepath = "C:\ps-test\test.txt"
$owner = "juneb"
$result = "Success"
Write-Verbose ("Created {0} by {1}. Result is {2}." –f $filepath, $owner, $result)
To:
$VerbosePreference = "Continue"
$filepath = "C:\ps-test\test.txt"
$owner = "juneb"
$result = "Success"
Write-Verbose "Created $filepath by $owner. Result is $result."
And it worked perfectly. So, I went through the script replacing the formatted strings with double-quoted strings.
Then I came upon statements like these, which use properties and nested properties of objects.
PS C:\> $p = Get-Process PowerShell
PS C:\>Write-Verbose ("The {0} process uses the {1} window style." -f $p.Name, $p.StartInfo.WindowStyle)
These statements return:
VERBOSE: The powershell process uses the Normal window style.
But, when I changed this Write-Verbose message to use the variables in a double-quoted string, it didn't work:
PS C:\> $p = Get-Process PowerShell
PS C:\> Write-Verbose "The $p.Name process uses the $p.StartInfo.WindowStyle window style."
VERBOSE: The System.Diagnostics.Process (powershell).Name process uses the System.Diagnostics.Process (powershell).StartInfo.WindowStyle window style.
In formatted strings, any expression can be a value. That's not true in double-quoted strings. When the values are expressions, the value type replaces the variable in the double-quoted string.
You can wrap the variable properties in another variable by using parentheses or braces. For example, change:
$<name>
To:
$($<name>)
Or:
${$<name>}
So, $p.Name becomes $($p.Name) or ${$p.Name}.
PS C:\> Write-Verbose "The $($p.Name) process uses the $($p.StartInfo.WindowStyle) window style."
VERBOSE: The powershell process uses the Normal window style.
It works, but it's not pretty.
Another obvious solution is to evaluate the expressions and save the results in variables before using them in the double-quoted string.
PS C:\> $p = Get-Process PowerShell
PS C:\> $pName = $p.Name
PS C:\> $pStyle = $p.StartInfo.WindowStyle
PS C:\> Write-Verbose "The $pName process uses the $pStyle window style."
VERBOSE: The powershell process uses the Normal window style.
That works, of course, but it's not more efficient. And, more importantly, when you're writing scripts that you want people to read and interpret, it's not always clearer.
Instead, the original formatted string is efficient, and when you understand string substitution, much clearer.
PS C:\> $p = Get-Process PowerShell
PS C:\>Write-Verbose ("The {0} process uses the {1} window style." -f $p.Name, $p.StartInfo.WindowStyle)
The format string is also handy when creating strings that have a syntax that is different from the Windows PowerShell syntax. In this case, Windows PowerShell tries to interpret the variable that precedes the colon as a drive name:
PS C:\> $urlHost = $url.Host
PS C:\> $ValuesPort = $values.Port
Write-Verbose ("Add-AzureVM: The url is https:// $urlHost:$valuesPort/msdeploy.axd" -Verbose
At line:1 char:49
+ Write-Verbose "Add-AzureVM: Publish Url https://$urlHost:$valuesPort/msdeploy ...
+ ~~~~~~~~~
Variable reference is not valid. ':' was not followed by a valid variable name character. Consider using ${} to delimit the name.
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : InvalidVariableReferenceWithDrive
Any of the following statements work as expected without creating extra variables, but I think that the format string makes the intended syntax much clearer than the alternatives.
Write-Verbose ("Add-AzureVM: The url is https://{0}:{1}/msdeploy.axd" -f $url.Host, $Values.Port) -Verbose
Write-Verbose ("Add-AzureVM: The url is https://$($url.Host):$($Values.Port)/msdeploy.axd" -Verbose
Write-Verbose ("Add-AzureVM: The url is https://${$url.Host}:${$Values.Port}/msdeploy.axd" -Verbose
Write-Verbose ("Add-AzureVM: The url is https://" + $url.Host + ":" + $Values.Port + "/msdeploy.axd") -Verbose
So, when the message string includes simple variables, I use double-quoted strings. When the message string includes a property value that is used repeatedly, I save the property value in a new variable and use the variable a double-quoted string.
But when a string includes multiple expressions or an alternate syntax, I prefer a formatted string.
And I learned an important lesson about using formatted strings and the -f operator in Windows PowerShell. It's not just an interesting artifact. It's really useful.
~June
Thanks for writing this informative post, June!
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy