Solving PowerShell Argument Input Errors with Wrapper Scripts
Here are 3 solutions for correctly passing strings with embedded spaces into PowerShell scripts
November 27, 2009
As a security measure, Windows PowerShell doesn't create an association between the script extension .ps1 and the PowerShell command shell. The result is that clicking on a file with the extension of .ps1 doesn't cause it to execute, and you can't just drag and drop files onto a PowerShell script icon for easy execution. You can create a Windows shortcut or a Cmd.exe batch file that wraps up the process of invoking PowerShell explicitly for a script. This workaround doesn't represent a security problem per se, because it doesn't create the vulnerabilities that a file-type association does. However, due to the way arguments are parsed and passed on by Windows, arguments that need to be enclosed in double quotes (e.g., they include spaces) aren't passed correctly to PowerShell scripts.
Fortunately, there are several ways around this problem; the most common ways involve a batch-file wrapper, which I refer to as a wrapper script for simplicity. I'll explain the problem and the basic workarounds as well as some points that might be important if you need to go further than I do here.
The Problem: Stripping Double Quotes
The easiest way to communicate the problem is to use an example. We start out with a simple PowerShell script, echodemo.ps1, which looks like this:
#echodemo.ps1
$i = 0;
foreach($arg in $args)
{$i++; "$i $arg"}
All this script does is loop through each of the arguments passed to it, writing an argument counter and the argument for each one. For ease of testing, I have the script saved in the same folder as a couple of files that have spaces in their names. As Figure 1 shows, when I run the script with the filenames as arguments, the script treats each quoted string as a separate argument.
PowerShell lets you specify command-line options when you start PowerShell, and by using the Command option, you can explicitly tell PowerShell to run a script or an internal command. You can see the detailed Help for PowerShell's command-line options by entering the following command at either a Cmd.exe or PowerShell.exe prompt:
powershell -?
We want to run commands that need command-line parameters passed into PowerShell. According to the PowerShell 1.0 Help documentation, we just need to use
powershell -Command
followed by a script block. This works fine—as long as there are no quotes needed for filenames or other parameters passed into PowerShell. If this sounds confusing, don't worry: In fact, it's simpler to run commands than the Help information indicates.
We're aided in the search for a solution by the fact that Cmd.exe command shell scripts have a special generic placeholder variable, %*, that substitutes in all unused arguments, putting quotes around them as needed. However, this process doesn't correctly run all commands in PowerShell. Listing 1 shows several Cmd.exe wrapper scripts I wrote to demonstrate how %* variable expansion works. These examples will let us explore what happens when you create a Cmd.exe wrapper script for running a PowerShell script that takes multiple quoted arguments.
Figure 2 shows the output from the first four scripts in Listing 1. The output from wrapper0 clearly shows that %* expansion substitutes the filenames, with quotes as needed. The wrapper1 script doesn't cause echodemo.ps1 to run at all; instead, it appears that PowerShell interprets everything within the script block brackets as a single string, and PowerShell simply echoes back what it received. In wrapper2, I include the PowerShell invocation operator, &, and quote the entire string. Now echodemo.ps1 runs, but the quotes are stripped from the quoted arguments. As a result, each word of each filename is interpreted as a separate argument. In wrapper3, I make a last-ditch attempt, which I knew would fail, quoting the argument expansion term %*. This method concatenates all of the command-line arguments into a single long argument.
So how do we pass data containing embedded spaces into PowerShell? We actually have our choice of solutions, and that's what the rest of this article is about.
Solution 1: PowerShell 2.0's File Parameter
PowerShell 2.0, which is in its Community Technology Preview (CTP) stage as I write, provides an alternative string parsing technique for running a script. You can use -File instead of -Command, followed by the path to the script and the arguments you wish to use with the script, and the arguments will be parsed correctly. So with PowerShell 2.0, we can use a batch-file wrapper such as the following command, which is also shown as wrapper4.cmd in Listing 1:
PowerShell -File echodemo.ps1 %*
PowerShell passes the arguments following the script path directly to the script as the pre-parsed $args array. The result is that each quoted argument appears as a separate argument. Of course, this method works only if you have PowerShell 2.0 installed. You can download the CTP3 version from the Microsoft Download Center at www.microsoft.com/downloads/details.aspx?FamilyID=c913aeab-d7b4-4bb1-a958-ee6d7fe307bc.
However, telling someone they need to upgrade is one of the top 10 most irritating responses to a technical problem, so let's explore how you solve this problem with PowerShell 1.0.
Solution 2: Turn Arguments into PowerShell 1.0 Input
To increase PowerShell's flexibility, you can use a hyphen (-) following the Command parameter to tell PowerShell to interpret input as command text to execute. In a batch file, this technique means you can pipe anything, including the output of an echo command, into PowerShell and it will be received without any further parsing or quote-stripping by Windows or Cmd.exe. The wrapper5.cmd batch file shown in Listing 1 uses this technique to make PowerShell correctly interpret quoted arguments.
By the way, PowerShell automatically exits after it finishes the script when you use wrapper5.cmd. This behavior is desirable if you're using the script to accomplish a task directly. However, if you want to inspect the output from the PowerShell script, you'll want to use PowerShell's NoExit parameter, as shown in wrapper6.cmd in Listing 1.
This technique also works in the PowerShell 2.0 CTP releases, so it should be fully forward-compatible. However, it still has one basic flaw. Many of PowerShell's special characters are used in variant but regrettably common filenames. Names that contain parentheses in particular can cause PowerShell to attempt evaluating the content as an embedded command. To solve such problems, we need a better way to handle passing commands to PowerShell. We need to process items so that PowerShell unambiguously treats them as file and folder names as well. Therefore, we'll need a stronger solution than a batch-file wrapper. Our last solution uses VBScript to correctly pass arguments to PowerShell.
Solution 3: Using a WSH Wrapper Script
Listing 2 shows a generic Windows Script Host (WSH) wrapper script that you can use as a drag-and-drop wrapper for any PowerShell script. To prepare the wrapper script, simply use the following steps:
Save a copy of the VBScript template to the same folder as the PowerShell script you wish to run, and make sure the basename (the bare name of the file) of the VBScript file is identical to the basename of the PowerShell script. For example, if the PowerShell script is C:appsScan-File.ps1, its basename is Scan-File. So you would save the VBScript template as C:appsScan-File.vbs. The VBScript wrapper can then figure out what the name of the PowerShell script is.
The VBScript wrapper runs the PowerShell script so that it automatically exits and closes when it's finished running. If you want to keep the PowerShell script running, go to the area in the script file where the base command is defined, callout A, and comment it out by inserting a single quote (VBScript's single-character comment marker). Then remove the initial single quote commenting out the base definition in callout B. If you want the script to go away when it finishes running, however, leave things as they are.
The VBScript wrapper runs the PowerShell script in a minimized window to make it as unobtrusive as possible. If you prefer to run the script with the PowerShell window appearing differently, go to callout C, the line that reads WshShell.Run Command, 2. The final numeral controls the window style. To run the window with the default output position and size, which is useful if you're going to look at output from the script or if it might prompt you for additional information, change the 2 to a 1. If the script doesn't prompt you for anything and doesn't display any output you need to see, you can change this number to 0, which keeps the window hidden. Choose this option only if you aren't keeping PowerShell running using the code in Callout B. Keeping the session running combined with running the PowerShell window hidden means that each time you use the script, you'll have a new, hidden PowerShell session that continues to run until you reboot or end the process from Task Manager.
If you like, create a shortcut to the WSH script on your desktop for easy access.
To use the PowerShell script, just drag and drop files and folders onto the WSH script or your shortcut to it. The script locates the PowerShell script based on the assumption that the PowerShell script uses the same name as the WSH script and is in the same folder, then starts assembling a command line for executing the PowerShell script.
First, the VBScript makes sure any spaces in the path to the script are escaped so that PowerShell won't interpret the name as multiple arguments. Next, the VBScript loops through the names of items dropped onto it. VBScript checks to ensure that each item is a real file or folder, discarding it otherwise. After the item is confirmed as a file-system item, VBScript checks for single quotes used as part of the filename and escapes them for PowerShell. Next, the name is surrounded with single quotes and saved in the collection of prepared arguments. When all arguments have been processed, the script assembles them into a PowerShell-safe command statement and runs it.
This process might seem like overkill for this kind of problem, but it's actually a reasonable solution. Other scripting languages occasionally make similar accommodations; for example, Perl uses similar batch files as wrappers for Perl scripts designed to run from a command prompt, and those batch scripts can sometimes be hundreds of lines long. When you know how you typically run your PowerShell drag-and-drop scripts, you won't even have any customization to do. All you'll generally have to do is copy your template and rename it to match your next PowerShell script that you want to use this technique with.
Choosing a Solution
Deciding which technique to use for making batch-file wrappers to pass quoted strings correctly to PowerShell is straightforward for most problems. If you can use PowerShell 2.0—when it's available—running Powershell.exe with the File parameter is a good solution to the problem because it correctly parses quoted filenames and other quoted input arguments. If for some reason you can't use PowerShell 2.0, and if your need is primarily drag-and-drop processing, a batch file wrapper written like wrapper5.cmd or wrapper6.cmd should do the trick. If you need Cmd.exe and PowerShell interoperability, you can use wrapper2.cmd with explicit single quoting where needed, or (preferably) work from PowerShell as your default command shell.
For drag-and-drop use, all you need to do is create the appropriate wrapper script in the same folder as the PowerShell script you want to use as your drop target; the resulting script should look like my wrappers, but with the name of your script substituted for echodemo.ps1.
The solutions should work for most scenarios, but there are situations where you might have trouble. PowerShell makes copious use of special characters that are occasionally also used in filenames. Those special characters that can cause odd behavior in specific situations include all of the brackets and parentheses as well as $, `, and '. Although it's possible to escape these characters so that PowerShell handles them correctly, it would be a complex job. This kind of task is best performed from a WSH wrapper script that explicitly parses an entire command sequence and escapes it before passing it on to PowerShell.
The scripts in this article are available for download by clicking the Download the Code Here button at the top of the page. For most uses, you should find that wrapper scripts 2, 5, and 6 work as templates for drag-and-drop PowerShell wrapper scripts that correctly handle filenames with embedded spaces.
Listing 1: Cmd.exe Wrapper Scripts that Demonstrate %* Variable Expansion
::wrapper0.cmd:: just expands and echoes arguments to standard outputecho %*::wrapper1.cmd:: Everything inside {} is echoed as a stringpowershell -Command { .echodemo.ps1 %* }::wrapper2.cmd:: Doublequotes are stripped out completelypowershell -Command "& {.echodemo.ps1 %* }"::wrapper3.cmd:: Expanded arguments become one string in '%*'powershell -Command "& {.echodemo.ps1 '%*' }"::wrapper4.cmd:: Works properly - PowerShell 2 onlypowershell -File .echodemo.ps1 %*::wrapper5.cmd:: Input processed as a command; works in PowerShell 1 and 2echo .echodemo.ps1 %* | powershell -Command -::wrapper6.cmd:: As wrapper5.cmd, but -NoExit keeps PowerShell running.echo .echodemo.ps1 %* | powershell -NoExit -Command -
Listing 2: Generic WSH Wrapper Script for Any PowerShell Script
' BEGIN WSH wrapper script' Should have same basename as PS script it will run,' and must be in same folder as PS script.' ex: if c:tmpfred.ps1, this must be c:tmpfred.vbsdim fso: Set fso = CreateObject("Scripting.FileSystemObject")Dim WshScript: WshScript = WScript.ScriptFullNameDim PsScriptPsScript = fso.BuildPath( _ fso.GetFile(WshScript).ParentFolder.Path, _ fso.GetBaseName(WshScript) & ".ps1")' Escape spaces embedded in script path, if any.PsScript = Replace(PsScript, " ", "` ")' Escape single quotes by doubling.PsScript = Replace(PsScript, "'", "'")Dim i, argi = 0Dim ArgSet: Set ArgSet = CreateObject("Scripting.Dictionary")Argset(i) = PsScriptFor each arg in WScript.Arguments' EXPLICITLY ensure these resolve to file/folder paths if fso.FileExists(arg) or fso.FolderExists(arg) then i = i + 1 ' Include escapes for singlequotes in paths, if any Argset(i) = "'" & Replace(arg, "'", "'") & "'" End IfNextDim base' BEGIN Callout Abase = "PowerShell -Command ""& {"' END Callout A' Use the following base instead to keep the window open.' BEGIN Callout B'base = "powershell -NoExit -Command ""& {"' END Callout BDim CommandCommand = base & Join(ArgSet.Items) & "}"""' WScript.Echo "command as passed to PowerShell:", CommandDim WshShell: Set WshShell = CreateObject("WScript.Shell")' Now run the command' BEGIN Callout CWshShell.Run Command, 2' END Callout C' END WSH Wrapper Script
About the Author
You May Also Like