Tuesday 18 June 2013

PowerShell Command Line Quoting Weirdy

My last post “Handling Paths with Spaces & Parenthesis in Batch Files” was really just a precursor to this one which is about another weird thing I bumped into recently with executing simple PowerShell one-liners via a batch file. I applied the same logic I always do to quoting long filenames [1] and ran into confusing behaviour that seems to create the exception to my rule of thumb…

PowerShell Strings

In PowerShell you can define strings with either double quotes (“string”) or single quotes (‘string’). The main difference is that the former will perform variable substitution (“Hello $FullName”) whereas the latter won’t. Essentially, if the string contains no variables or expressions they are identical and as such if I create a PowerShell script with the following two statements I should get the same behaviour:-

Get-ChildItem "C:\Program Files" | Measure-Object | Select Count
Get-ChildItem 'C:\Program Files' | Measure-Object | Select Count

Both do execute fine and I get a simple table with the same answer, twice:-

Count
-----
   55
   55

PowerShell One-Liners

So far so good. But what about if I want to execute the same simple one liner as part of a batch file? I don’t want to have to ship another .ps1 script so I can put the whole thing on the PowerShell command line and execute it directly like this:-

C:\> PowerShell Get-ChildItem C:\ ^| Measure-Object ^| Select Count

Obviously the pipe symbol means something to the CMD.EXE shell and so I need to escape it with the caret (^) character. This works a treat and I get the output I expect. The next step is to generalise it, then I can use it in a batch file. So I’ll create a variable and then follow my own advice and surround it with double quotes to allow for long filenames:-

set folder=%~1

PowerShell Get-ChildItem "%folder%" ^| Measure-Object ^| Select Count

Huh? When I run this I get a rather unexpected error:-

Get-ChildItem : Cannot find path 'C:\Program' because it does not exist.
At line:1 char:14
+ Get-ChildItem <<<<  C:\Program Files | Measure-Object | Select Count

Originally my gut reaction was to try the second form - using single quotes - to see if that worked instead:-

PowerShell Get-ChildItem '%folder%' ^| Measure-Object ^| Select Count

And it does. However, another way to deal with escaping the pipe symbols is to provide the entire command as a single argument by surrounding it with double quotes, like so:-

PowerShell "Get-ChildItem C:\ | Measure-Object | Select Count"

Good, we’re back to green again. A common technique for embedding double quotes within an argument is to escape them with a backslash. And that, it seems, also works:-

PowerShell "Get-ChildItem \"%folder%\" | Measure-Object | Select Count"

Hmmm, unwinding the trial-and-error stack and applying the same escaping policy to the original command line now gets us what we wanted from the beginning:-

set folder=%~1

PowerShell Get-ChildItem \"%folder%\" ^| Measure-Object ^| Select Count

. . .

Count
-----
   55

Who’s Escaping From Whom?

The whole trial-and-error thing really makes me nervous as I don’t know what other error scenario I might have just traded to get this one working. When it comes to escaping command lines you’re running from a shell you need to factor in what escaping policy the shell uses along with what policy the application’s runtime uses. Just to make sure I know where this escaping is being handled I can take the CMD.EXE shell out of the equation by running the command via Explorer’s “Run…” option (Win+R) or via Task Manager’s “File | New Task…” command.

PowerShell Start-Transcript C:\Temp\Test.txt; Get-ChildItem \"C:\Program Files\" | Measure-Object | Select Count

Running the actual statement causes a PowerShell window to appear and disappear within a blink of an eye, so by prefixing the command with the Start-Transcript statement I can capture the output to a text file as if I was redirecting via a shell. Lo and behold it needs the same escape sequence - \” - to work and so I now know it’s PowerShell that for some reason needs the extra escaping (or use of single quotes) instead of the shell.

 

[1] I wonder how many developers today remember the (good|bad) old days of “short” filenames? For the uninitiated the term long filename is often synonymous with a path that contains spaces as short filenames (8.3 - filename.ext) never [officially] supported them.

No comments:

Post a Comment