Sunday, March 3, 2013

PowerShell string literals and formating


Basics

String literals are one of essential constructs of PS. There are lot of ways to create and manage them.

There is a major difference between single and double quoted string. Single quoted literals are not processed by PowerShell at all, so you can not add variables, escaped characters and other cool stuff to them.
  1. $var = 'Append'  
  2.   
  3. # Substitution:  
  4. Write-Host 'No substitution: $var' # No substitution: $var  
  5. Write-Host "Substitution: $var"    # Substitution: Append  
  6.   
  7. # Double quote char to add same quotes as literal quoted:  
  8. Write-Host 'Don''t forget that " is originally inch character' # Single quote doubled  
  9. Write-Host "Don't forget that "" is originally inch character" # Double quote doubled 
  10.  
  11. # To use escape chars escape them with ` character. 
  12. # Find it on [~] key, it is like small back slash. 
  13. # It works only with double quoted strings 
  14. Write-Host "First line`r`nSecong line"  # line break 
  15. Write-Host "First`tSecond"              # tabulation  
The benefit of single quoted strings is easy way to create string with lots of '$', '`' and '"' characters in it.

Expression substitution

First advanced feature I want to mention is a complex expression substitutions. You are not limited with simple variable substitution in double quoted strings. You can add expressions of any complexity to your string, and they will be executed. Just wrap expression inside double quoted string with $( ... ):
  1. $arr = @(4, 8, 15, 16, 23, 42)  
  2. $hashtable = @{  
  3.     Name = "James";  
  4.     Surname = 'Bond';  
  5. }  
  6. Function Add-Numbers ($a,$b) {  
  7.     $a + $b  
  8. }  
  9.   
  10. Write-Host "Answer to the Ultimate Question of Life is $($arr[5])"  
  11. Write-Host "My name is $($hashtable.Surname), $($hashtable.Name) $($hashtable.Surname)."  
  12. Write-Host "13 + 43 = $(Add-Numbers 13 41)"  

Here-strings

When you want to add some big literal with line breaks, both single and double quotes in it (great example is XML) you do not need to shield all them and change line breaks to "`r`n" stuff. There is such thing as 'here-strings' in PowerShell. They should start with @" and line break and should end with line break "@.
  1. # Notice that line break after @" and before "@ is not included to string  
  2. $hereString = @" 
  3. First line 
  4. Quotes of both types: " and '  
  5. Grave accent: `  
  6. Last line  
  7. "@  

Format string

You can use ordinary format string via special keyword -f. As in .NET you can use format item to add some additional parameters to format:
  1. Write-Host ( 'My name is {1}, {0} {1}' -f 'James''Bond' )  
  2. # My name is Bond, James Bond  
  3.   
  4. Write-Host ( 'Double formatting {0:F3}' -f 1.2 )  
  5. # Double formatting 1,200  
  6.   
  7. Write-Host ( 'DateTime format {0:MM/dd/yy H:mm:ss zzz}' -f [DateTime]::Now)  
  8. # DateTime format 03.03.13 21:32:34 +02:00  
Please notice, that you can not feed other cmdlets result of format without round brackets, otherwise it will be interpreted as attribute name of feeded cmdlet:
  1. # Error!  
  2. Write-Host 'Simple {0}' -f 'format'  
Here -f will be threated like Write-Host -Foreground attribute, so just wrap it with ( ) as in examples above.

Performance

In rare cases single quoted string can give you performance boost if you prefer concatenation of single quoted strings to substitution inside of double quoted string. Try this example, and you will find, that concatenation is almost two times faster:
  1. $s = New-Object System.Diagnostics.Stopwatch  
  2.   
  3. $s.Start()  
  4. for ($i = 0; $i -lt 1000000; $i++) {  
  5.     $var = 'world'  
  6.     $a = "Hello $var"  
  7. }  
  8. $s.Stop()  
  9. Write-Host 'Double quotes with substitution: ' + $s.Elapsed  
  10.   
  11.   
  12. $s.Reset()  
  13. $s.Start()  
  14. for ($i = 0; $i -lt 1000000; $i++) {  
  15.     $var = 'world'  
  16.     $a = 'Hello ' + $var  
  17. }  
  18. $s.Stop()  
  19. Write-Host 'Single quotes and concatenation: ' + $s.Elapsed  

Tuesday, February 26, 2013

PowerShell ISE sessions and tabs

Many times I heard a question "How to wipe/clean ISE session?". It is easy. You do not need to close whole ISE.

All opened files in ISE share same Runtime and so do all global variables.

To create new Runtime just create new tab: Ctrl + T.
Before
After

New tab ( "Powershell 4" in this case ) will start with new, clean Runspace.

PowerShell XML

Today I want to share with you several common approaches in XML exploration and modification.

This is our test XML:
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <configuration>  
  3.   <startup>  
  4.     <supportedRuntime version="4.0" />  
  5.   </startup>  
  6.   <appSettings>  
  7.     <add key="UseSuperpowers" value="false" />  
  8.     <add key="IgnoreKryptonite" value="true" />  
  9.   </appSettings>  
  10. </configuration>  

Load XML

  1. [xml]$configXml = Get-Content -Path C:\TestFile.xml  
Notice, that we cast loaded text to [xml] type. Actually $configXml is System.Xml.XmlDocument instance.
If file contains corrupted xml or no xml at all you will get exception.


This cast is very interesting fact, at first I thought that PS searches for constructor of given type which accepts argument of left expression. But XmlDocument has neither constructor nor static method (something like Load(string)) which accepts string. PowerShell magic.
This process is called Attribute Transformation. It allows to perform hidden transformation of types in freestyle way (like instantiate XmlDocument and invoke its method LoadXml(string) instead of type cast). You can start your own investigation from here: http://goo.gl/iZEGA.

Navigate XML

Another PS magic issue. As you can notice, we can use our xml variable like dynamic to access its attributes or nodes.
  1. # Access node  
  2. Write-Host $configXml.configuration.startup.supportedRuntime.OuterXml  
  3. # Output: <supportedRuntime version="4.0" />  
  4.   
  5. # Access attribute  
  6. Write-Host $configXml.configuration.startup.supportedRuntime.version   
  7. # Output: 4.0  
  8.   
  9. # Get colletion of nodes  
  10. $configXml.configuration.appSettings.add.Count                     
  11. # Output: 2  
  12.   
  13. # Iterate collection of nodes  
  14. $configXml.configuration.appSettings.add | %{ Write-Host $_.key }      
  15. # Output: UseSuperpowers IgnoreKryptonite  
If this 'dynamic path' points to several elements (like configuration.appSettings.add in our case) we will get array of nodes.

Navigate via XPath

You can use XPath to select nodes and attributes with Select-Xml cmdlet. It does not return elements itself but SelectXmlInfo instances. Use their Node property to get node itself:
  1. # Query node  
  2. $XPath = "/configuration/startup/supportedRuntime"  
  3. $node = ( $configXml | Select-Xml -XPath $XPath ).Node  
  4. Write-Host $node.OuterXml  
  5. # Output: <supportedRuntime version="4.0" />  
  6.   
  7. # Query attribute with condition  
  8. $XPath = "/configuration/appSettings/add[@key='IgnoreKryptonite']/@value"  
  9. $node = ( $configXml | Select-Xml -XPath $XPath ).Node  
  10. Write-Host $node.Value  
  11. # Output: false  

Edit XML

To edit XML you need to navigate to any element via one of approaches described and change its InnerXML or InnetText properties (in case of attribute you need to change Value property):
  1. # Change attribute 1  
  2. $configXml.configuration.startup.supportedRuntime.version = "5.0"  
  3. # Will change attribute: <supportedRuntime version="5.0" />  
  4.   
  5. # Change attribute 2  
  6. $XPath = "/configuration/appSettings/add[@key='UseSuperpowers']/@value"    
  7. $node = ( $configXml | Select-Xml -XPath $XPath ).Node  
  8. $node.Value = "true"  
  9. # Will change attribute: <add key="UseSuperpowers" value="true" />    
  10.   
  11. # Add XML  
  12. $configXml.InnerXml += "<!-- End -->"  
  13. # Will add this comment to the very end  

Create XML

  1. # Append attribute  
  2. $newAttribute = $configXml.CreateAttribute('levelOfAwesome')  
  3. $newAttribute.Value = "Highest"  
  4. $configXml.configuration.startup.supportedRuntime.Attributes.Append($newAttribute)  
  5.   
  6. # Insert comment before tag  
  7. $newComment = $configXml.CreateComment('This section is all about supported runtime')  
  8. $startupNode = $configXml.configuration.startup;  
  9. $startupNode.InsertBefore($newComment$startupNode.supportedRuntime)  
  10.   
  11. # Append node with attributes  
  12. $newNode = $configXml.CreateElement('add')  
  13.   
  14. $newAttribute = $configXml.CreateAttribute('key')  
  15. $newAttribute.Value = "wearUnderwearOverPants"  
  16. $newNode.Attributes.Append($newAttribute)  
  17.   
  18. $newAttribute = $configXml.CreateAttribute('value')  
  19. $newAttribute.Value = "true"  
  20. $newNode.Attributes.Append($newAttribute)  
  21.   
  22. $configXml.configuration.appSettings.AppendChild($newNode)  

Save XML

Use $configXml.Save($filePath) to save human readable XML, because other approaches (like Set-Content -Path $someFile -Value $configXml.InnerXml) will loose tabulation and formatting and save all XML in one line:
  1. Set-Content -Path C:\test.xml $configXml.InnerText  
  2. # Result:   
  3. #  <?xml version="1.0" encoding="utf-8"?><configuration><startup><!--This section is all about supported runtime--><supportedRuntime version="4.0" levelOfAwesome="Highest" /></startup><appSettings><add key="UseSuperpowers" value="false" /><add key="IgnoreKryptonite" value="true" /></appSettings></configuration>  
  4.   
  5. $configXml.Save('C:\test.xml')  
  6. # Result:  
  7. #  <?xml version="1.0" encoding="utf-8"?>    
  8. #  <configuration>    
  9. #    <startup>    
  10. #      <supportedRuntime version="4.0" />    
  11. #    </startup>    
  12. #    <appSettings>    
  13. #      <add key="UseSuperpowers" value="false" />    
  14. #      <add key="IgnoreKryptonite" value="true" />    
  15. #    </appSettings>    
  16. #  </configuration>  

Friday, August 31, 2012

One character drama


I work with several large brown field PowerShell projects, which were developed by different peoples, both amateurs and pro in PS. I saw different, ugly and beautiful things.

The most confusing are one char syntax, that powershell is rich of. Some of this are common, some of this are rare and even redundant.
Here is short listing:
  1. # Explicit provider  
  2. Get-Content -Path Function:prompt  
  3.   
  4. # Old good string format  
  5. 'This is my post about {1} number {0}' -F 3, 'powershell'  
  6.   
  7. # Escaping  
  8. "First line`r`nSecond one"  
  9.   
  10. # Explicit scope  
  11. $Global:test = 'GlobalTest'  
  12.   
  13. # Hashtable 
  14. $hash = @{ Name = 'Hashtable'; Description = 'Ordinary Collections.Hashtable'; }  
  15.   
  16. # Call operators  
  17. & 'Get-Process'  
  18. . 'Get-Process'  
  19.   
  20. # Use bool in switch argument  
  21. $someBool = $true  
  22. New-Item -Path Variable:SwitchTest -Value 1 -Verbose:$someBool  
  23.   
  24. # Simple scriptblock  
  25. $sb = { Get-Process }   
  26.   
  27. # Foreach-Object and Where-Object aliases  
  28. Get-Process | ?{ $_.PM -gt 1000 } | %{ $_.ProcessName }  

Item or array?

PowerShell cmdlets behave in interesting way. You can get an array or a single object from one method, so, be ready!
Innocent code snippet:

  1. $processes = Get-Process -Name powershell* -Verbose  
  2. If ($processes.count -gt 1) {  
  3.     "There are several powershell consoles"  
  4. } ElseIf ($processes.count -eq 1) {  
  5.     "There is one powershell console"  
  6. } Else {  
  7.     "There are no powershell consoles at all"  
  8. }  
What can go wrong?

But PowerShell runtime does a following thing:
  • If cmdlet returns several objects, it wraps them in object[] 
  • If only one object is returned to pipe, it returns the object itself (Process class instance in our case)
  • If no results are returned, it returns $null
You will get  "There are no powershell consoles at all" when you have both 0 or 1 console.

The preferred way to work with cmdlet output is pipeline, so most clean and robust solution will be:
  1. $powershellConsoles = 0  
  2. Get-Process -Name powershell* | ForEach-Object { $powershellConsoles++ }  
  3.   
  4. # First way, generic    
  5. If ($powershellConsoles -gt 1) {    
  6.     "There are several powershell consoles"    
  7. } ElseIf ($powershellConsoles -eq 1) {    
  8.     "There is one powershell console"    
  9. } Else {    
  10.     "There are no powershell consoles at all"    
  11. }  

Also, you can handle output like that:
  1. $processes = Get-Process -Name powershell*
  2.   
  3. # First way, generic  
  4. If ($processes -is [array]) {  
  5.     "There are several powershell consoles"  
  6. } ElseIf ($processes -ne $null) {  
  7.     "There is one powershell console"  
  8. } Else {  
  9.     "There are no powershell consoles at all"  
  10. }  
  11.   
  12. # Second way, straight typed  
  13. If ($processes -is [array]) {  
  14.     "There are several powershell consoles"  
  15. } ElseIf ($processes -is [System.Diagnostics.Process]) {  
  16.     "There is one powershell console"  
  17. } Else {  
  18.     "There are no powershell consoles at all"  
  19. }  

Also, notice, that default cmdlets behavior is to write error, if explicit conditions were determined:
  1. # Will silently return $null, if there are no jobs at all  
  2. Get-Job  
  3.   
  4. # Will return null and error message, if there are no jobs reaching conditions  
  5. Get-Job -Name NonexistableJob  

The very beginning

During my development studies, I found a lot of interesting and twisted stuff (sure thing, powershell is the leader!), which I want to share.