Powershell call msbuild with nested quotation marks
Asked Answered
P

7

26

Using Powershell and Psake to create package and deployment for a visual studio solution. Trying to deploy a database project using msbuild - which is working correctly using msdos visual studio command line

   msbuild /target:Deploy /p:UseSandboxSettings=false /p:TargetConnectionString="aConnectionWithSpacesAndSemiColons" "aDatabaseProjectPathWithSpaces"

the same method call results in an error when called from powershell

& msbuild /target:Deploy /p:UseSandboxSettings=false /p:TargetConnectionString="aConnectionWithSpacesAndSemiColons" "aDatabaseProjectPathWithSpaces"

relating to spaces - can't figure out how to replicate this call in powershell - sample database connectionstring Data Source=.\SQL2008;Initial Catalog=DocumentExecution;Integrated Security=True;

Peer answered 3/6, 2011 at 8:17 Comment(0)
T
62

Short Version

How do you pass an argument containing quotes into a native command from PowerShell?

  • Use single quotes instead of double quotes in the argument string:
       "/p:Target='Data Source=(local)\SQL;Integrated Security=True'"
    /p:Target='Data Source=(local)\SQL;Integrated Security=True'

  • Use backslash-escaping for double quotes in the argument string:
       '/p:Target=\"Data Source=(local)\SQL;Integrated Security=True\"'
    /p:Target="Data Source=(local)\SQL;Integrated Security=True"

If the embedded quotes are only being used to treat the argument as a single string, rather than being a required part of the parameter, then the following can be used:

  • Quote the entire argument string, instead of embedding quotes in the argument:
       '/p:Target=Data Source=(local)\SQL;Integrated Security=True'
    /p:Target=Data Source=(local)\SQL;Integrated Security=True

  • Escape all PowerShell special characters with backticks (this can only be done as an in-line argument):
       /p:Target=`"Data Source=`(local`)\SQL`;Integrated Security=True`"
    or /p:Target=Data` Source=`(local`)\SQL`;Integrated` Security=True
    /p:Target=Data Source=(local)\SQL;Integrated Security=True


Full command line example (using the second alternative):

PS> [string[]]$arguments = @(
  '/target:Deploy',
  '/p:UseSandboxSettings=False',
  '/p:TargetDatabase=UpdatedTargetDatabase',
  '/p:TargetConnectionString=\"Data Source=(local)\SQL;Integrate Security=True\"',
  'C:\program files\MyProjectName.dbproj'
)
PS> ./echoargs $arguments
Arg 0 is </target:Deploy>
Arg 1 is </p:UseSandboxSettings=False>
Arg 2 is </p:TargetDatabase=UpdatedTargetDatabase>
Arg 3 is </p:TargetConnectionString="Data Source=(local)\SQL;Integrate Security=True">
Arg 4 is <C:\program files\MyProjectName.dbproj>



Long Version

Calling native commands is something that crops up quite a bit as folks move between the legacy cmd system and PowerShell (almost as much as the "separating parameters with commas" gotcha ;).

I've tried and sum up everything I know on the subject of command invocation in PowerShell (v2 and v3) here, along with all the examples and references I can muster.


1) Calling Native Commands Directly

1.1) At its simplest, for an executable located in the environment path, the command can be called directly, just as you would call a PowerShell cmdlet.

PS> Get-ItemProperty echoargs.exe -Name IsReadOnly
...
IsReadOnly   : True    

PS> attrib echoargs.exe
A    R       C:\Users\Emperor XLII\EchoArgs.exe


1.2) Outside of the environment path, for commands in a specific directory (including the current one), the full or relative path to the command can be used. The idea is to have the operator declare explicitly "I want to invoke this file", rather than let an arbitrary file that happened to have the same name get run in its place (see this question for more info on PowerShell security). Failing to use a path when it is required will result in a "term is not recognized" error.

PS> echoargs arg
The term 'echoargs' is not recognized as the name of a cmdlet, function, script file,
 or operable program...

PS> ./echoargs arg
Arg 0 is <arg>

PS> C:\Windows\system32\attrib.exe echoargs.exe
A    R       C:\Users\Emperor XLII\EchoArgs.exe


1.3) If a path contains special characters, the call operator or escape character can be used. For example, an executable starting with a number, or located in a directory containing a space.

PS> $env:Path
...;C:\tools\;...

PS> Copy-Item EchoArgs.exe C:\tools\5pecialCharacter.exe
PS> 5pecialCharacter.exe special character
Bad numeric constant: 5.

PS> & 5pecialCharacter.exe special character
Arg 0 is <special>
Arg 1 is <character>

PS> `5pecialCharacter.exe escaped` character
Arg 0 is <escaped character>


PS> C:\Users\Emperor XLII\EchoArgs.exe path with spaces
The term 'C:\Users\Emperor' is not recognized as the name of a cmdlet, function,
 script file, or operable program...

PS> & 'C:\Users\Emperor XLII\EchoArgs.exe' path with spaces
Arg 0 is <path>
Arg 1 is <with>
Arg 2 is <spaces>

PS> C:\Users\Emperor` XLII\EchoArgs.exe escaped` path with` spaces
Arg 0 is <escaped path>
Arg 1 is <with spaces>


2) Calling Native Commands Indirectly

2.1) When you are not typing out a command interactively, but instead have the path stored in a variable, the call operator can also be used to invoke the command named in a variable.

PS> $command = 'C:\Users\Emperor XLII\EchoArgs.exe'
PS> $command arg
Unexpected token 'arg' in expression or statement.

PS> & $command arg
Arg 0 is <arg>


2.2) The arguments passed to a command can also be stored in variables. Arguments in variables can be passed individually, or in an array. For variables containing spaces, PowerShell will automatically escape the spaces so that the native command sees it as a single argument. (Note that the call operator treats the first value as the command and the remaining values as arguments; the arguments should not be combined with the command variable.)

PS> $singleArg = 'single arg'
PS> $mushedCommand = "$command $singleArg"
PS> $mushedCommand
C:\Users\Emperor XLII\EchoArgs.exe single arg

PS> & $mushedCommand
The term 'C:\Users\Emperor XLII\EchoArgs.exe single arg' is not recognized as the
 name of a cmdlet, function, script file, or operable program...

PS> & $command $singleArg
Arg 0 is <single arg>

PS> $multipleArgs = 'multiple','args'
PS> & $command $multipleArgs
Arg 0 is <multiple>
Arg 1 is <args>


2.3) The array format is especially useful for building up a dynamic list of arguments for a native command. For each argument to be recognized as a distinct parameter, it is important that the arguments get stored in an array variable, and not just munged together in one string. (Note that the common abbreviation $args is an automatic variable in PowerShell, which can cause values saved in it to get overwritten; instead, it is better to use a descriptive name like $msbuildArgs to avoid the naming conflict.)

PS> $mungedArguments = 'initial argument'
PS> $mungedArguments += 'second argument'
PS> $mungedArguments += $(if( $someVariable ) { 'dynamic A' } else { 'dynamic B' })
PS> ./echoargs $mungedArguments
Arg 0 is <initial argumentsecond argumentdynamic B>

PS> $arrayArguments = @('initial argument')
PS> $arrayArguments += 'second argument'
PS> $arrayArguments += $(if( $someVariable ) { 'dynamic A' } else { 'dynamic B' })
PS> ./echoargs $arrayArguments
Arg 0 is <initial argument>
Arg 1 is <second argument>
Arg 2 is <dynamic B>


2.4) Also, for scripts, functions, cmdlets, and the like, PowerShell v2 can send named arguments contained in a hashtable using a technique called "splatting", without having to worry about parameter order. This does not work with native commands, which do not participate in the PowerShell object model and can only handle string values.

PS> $cmdletArgs = @{ Path = 'EchoArgs.exe'; Name = 'IsReadOnly' }
PS> $cmdlet = 'Get-ItemProperty'
PS> & $cmdlet $cmdletArgs     # hashtable object passed to cmdlet
Cannot find path 'C:\Users\Emperor XLII\System.Collections.Hashtable'...

PS> & $cmdlet @cmdletArgs     # hashtable values passed to cmdlet
...
IsReadOnly   : True

PS> ./echoargs @cmdletArgs
Arg 0 is <Name>
Arg 1 is <IsReadOnly>
Arg 2 is <Path>
Arg 3 is <EchoArgs.exe>


3) Calling Native Commands With Complicated Arguments

3.1) For simple arguments, the automatic escaping used for native commands is generally sufficient. However, for parenthesis, dollar signs, spaces, and such, characters used by PowerShell need to be escaped to be sent as-is to native commands, without having them interpreted by the parser. This can be done with the backtick escape character, `, or by putting the argument inside a single-quote string.

PS> ./echoargs money=$10.00
Arg 0 is <money=.00>

PS> ./echoargs money=`$10.00
Arg 0 is <money=$10.00>


PS> ./echoargs value=(spaces and parenthesis)
The term 'spaces' is not recognized as the name of a cmdlet, function, script file,
 or operable program...

PS> ./echoargs 'value=(spaces and parenthesis)'
Arg 0 is <value=(spaces and parenthesis)>


3.2) Unfortunately, this is not so simple when double quotes are involved. As part of argument processing for native commands, the PowerShell processor attempts to normalize all double quotes in an argument so that the contents of the argument, sans quotes, is passed as a single value to the native command. The native command parameter processing occurs as a separate step after parsing, so normal escaping will not work for double quotes; only escaped single quotes, or backslash-escaped double quotes can be used.

PS> ./echoargs value="double quotes"
Arg 0 is <value=double quotes>

PS> ./echoargs 'value="string double quotes"'
Arg 0 is <value=string>
Arg 1 is <double>
Arg 2 is <quotes>

PS> ./echoargs value=`"escaped double quotes`"
Arg 0 is <value=escaped double quotes>

PS> ./echoargs 'value=\"backslash escaped double quotes\"'
Arg 0 is <value="backslash escaped double quotes">


PS> ./echoargs value='single quotes'
Arg 0 is <value=single quotes>

PS> ./echoargs "value='string single quotes'"
Arg 0 is <value='string single quotes'>

PS> ./echoargs value=`'escaped` single` quotes`'
Arg 0 is <value='escaped single quotes'>


3.3) PowerShell v3 added a new stop-parsing symbol --% (see about_Parsing). When used before complicated arguments, --% will pass arguments as-is without any parsing or variable expansion, except for cmd-like %ENVIRONMENT_VARIABLE% values.

PS> ./echoargs User:"$env:UserName" "Hash"#555
Arg 0 is <User:Emperor XLII>
Arg 1 is <Hash>

PS> ./echoargs User: "$env:UserName" --% "Hash"#555
Arg 0 is <User:Emperor XLII>
Arg 1 is <Hash#555>

PS> ./echoargs --% User: "%USERNAME%" "Hash"#555
Arg 0 is <User:Emperor XLII>
Arg 1 is <Hash#555>

This can also be used to de-munge a single string representing multiple arguments, by passing the stop-parsing symbol in a string (although the best practice is to not munge arguments in the first place).

PS> $user = 'User:"%USERNAME%"'
PS> $hash = 'Hash#' + $hashNumber
PS> $mungedArguments = $user,$hash -join ' '
PS> ./echoargs $mungedArguments
Arg 0 is <User:%USERNAME% Hash#555>

PS> ./echoargs --% $mungedArguments
Arg 0 is <$mungedArguments>

PS> ./echoargs '--%' $mungedArguments
Arg 0 is <User:Emperor XLII>
Arg 1 is <Hash#555>


4) Debugging Native Commands

There are two key tools for debugging the arguments PowerShell passes to native commands.

4.1) The first is EchoArgs.exe, a console application from the PowerShell Community Extensions that simply writes back the arguments passed to it between angle brackets (as shown in the examples above).

4.2) The second is Trace-Command, a cmdlet that can show many details of how PowerShell processes a pipeline. In particular, the NativeCommandParameterBinder trace source will show what PowerShell receives and passes on to a native command.

PS> Trace-Command *NativeCommand* { ./echoargs value="double quotes" } -PSHost
DEBUG: NativeCommandParameterBinder : Raw argument string:  "value=double quotes"
DEBUG: NativeCommandParameterBinder : Argument 0: value=double quotes

PS> Trace-Command *NativeCommand* { ./echoargs 'value="double quotes"' } -PSHost
DEBUG: NativeCommandParameterBinder : Raw argument string:  "value="double quotes""
DEBUG: NativeCommandParameterBinder : Argument 0: value=double
DEBUG: NativeCommandParameterBinder : Argument 1: quotes

PS> Trace-Command *NativeCommand* { ./echoargs value=`"double quotes`" } -PSHost
DEBUG: NativeCommandParameterBinder : Raw argument string:  value="double quotes"
DEBUG: NativeCommandParameterBinder : Argument 0: value=double quotes

PS> Trace-Command *NativeCommand* { ./echoargs 'value=\"double quotes\"' } -PSHost
DEBUG: NativeCommandParameterBinder : Raw argument string:  "value=\"double quotes\""
DEBUG: NativeCommandParameterBinder : Argument 0: value="double quotes"


Other Resources

Articles

Questions

Terenceterencio answered 12/12, 2011 at 0:29 Comment(2)
rkeithhill.wordpress.com/2012/01/02/…Darrel
One of the best and most complete answers I've seen on StackOverflow!Botti
T
11

This could all be made a lot easier if you used the Start-Process cmdlet with the -ArgumentList parameter. I'm surprised that this hasn't been mentioned already.

Example:

Start-Process -FilePath msbuild.exe -ArgumentList '/target:Deploy /p:UseSandboxSettings=false /p:TargetConnectionString="aConnectionWithSpacesAndSemiColons" "aDatabaseProjectPathWithSpaces"';

Here's a method I like to use a bit better, which allows for variable substitution:

$ConnectionString = 'aConnectionWithSpacesAndSemiColons';
$DatabaseProjectPath = 'aDatabaseProjectPathWithSpaces';
$MsbuildArguments = '/target:Deploy /p:UseSandboxSettings=false /p:TargetConnectionString="{0}" "{1}"' -f $ConnectionString, $DatabaseProjectPath;
Start-Process -FilePath msbuild.exe -ArgumentList $MsbuildArguments;
Tansey answered 3/3, 2012 at 5:7 Comment(0)
M
4

Put the whole parameter in single quotes:

& msbuild /target:Deploy /p:UseSandboxSettings=false '/p:TargetConnectionString="aConnectionWithSpacesAndSemiColons"' "aDatabaseProjectPathWithSpaces"

The extra level of quoting will mean that PSH doesn't process the content with PSH's rules. (Any single quotes inside the string need to be doubled up—this is the only type of escaping in a PSH single quoted string).

Malapert answered 3/6, 2011 at 8:26 Comment(0)
P
1

@Richard - Testing this generates a different error saying that no valid project file is provided. I've run this through echoargs pscx helper to show some more detailed examples.

  1. With single quoatation marks wrapping the TargetConnectionString - Powershell evaluates each space in the connectionstring as a new line:

    & echoargs /target:Deploy /p:UseSandboxSettings=false    /p:TargetDatabase=UpdatedTargetDatabase /p:TargetConnectionString='"Data Source=(local)\SQLEXPRESS;Integrated Security=True;Pooling=False"' "C:\program files\MyProjectName.dbproj"
    
    Arg 0 is </target:Deploy>
    Arg 1 is </p:UseSandboxSettings=false>
    Arg 2 is </p:TargetDatabase=UpdatedTargetDatabase>
    Arg 3 is </p:TargetConnectionString=Data>
    Arg 4 is <Source=(local)\SQLEXPRESS;Integrated>
    Arg 5 is <Security=True;Pooling=False>
    Arg 6 is <C:\program files\MyProjectName.dbproj>
    
  2. Separating each parameter with backticks recreates the initial problem = no quotation marks around the connectionstring:

    & echoargs /target:Deploy `
    /p:UseSandboxSettings=false `
    

    c /p:TargetConnectionString="Data Source=(local)\SQLEXPRESS;Integrated Security=True;Pooling=False" "C:\program files\MyProjectName.dbproj"

    Arg 0 is </target:Deploy>
    Arg 1 is </p:UseSandboxSettings=false>
    Arg 2 is </p:TargetDatabase=UpdatedTargetDatabase>
    Arg 3 is </p:TargetConnectionString=Data Source=(local)\SQLEXPRESS;Integrated Se
    curity=True;Pooling=False>
    Arg 4 is <C:\program files\MyProjectName.dbproj>
    
  3. Adding backticks to quotation marks behaves the same as example 1:

    & echoargs /target:Deploy `
    /p:UseSandboxSettings=false `
    /p:TargetDatabase=UpdatedTargetDatabase `
    "/p:TargetConnectionString=`"Data Source=(local)\SQLEXPRESS;Integrated Security=True;Pooling=False"`"  `
    "C:\program files\MyProjectName.dbproj"
    
  4. Using the @ operator to try to split the parameters still ignores the quotes:

    $args = @('/target:Deploy','/p:UseSandboxSettings=false','     /p:TargetDatabase=UpdatedTargetDatabase','/p:TargetConnectionString="Data Source=(local)\SQLEXPRESS;Integrated Security=True;Pooling=False"','C:\program files\MyProjectName.dbproj'); $args 
    
    /target:Deploy
    /p:UseSandboxSettings=false
    /p:TargetDatabase=UpdatedTargetDatabase
    /p:TargetConnectionString="Data Source=(local)\SQLEXPRESS;Integrated           Security=True;Pooling=False"
    C:\program files\MyProjectName.dbproj
    
    & echoargs $args
    
  5. Backticks to escape the connectionstring using line separators - same results as example 1:

    & echoargs /target:Deploy `
    /p:UseSandboxSettings=false `
    /p:TargetDatabase=UpdatedTargetDatabase `
    "/p:TargetConnectionString=`"Data Source=(local)\SQLEXPRESS;Integrated Security=True;Pooling=False"`" `
    "C:\program files\MyProjectName.dbproj"
    
Peer answered 4/6, 2011 at 0:53 Comment(1)
I've had luck in the past with your example 4, and a quick test on my machine preserves the quotes. The variable $args has special meaning in powershell; you might try a different name, like $msbuildArgs.Terenceterencio
A
1

Your problem is that PowerShell does not escape quotes when it passes them to command line applications. I ran into this myself and thought that PowerShell was eating the quotes. Just do this.

msbuild /target:Deploy /p:UseSandboxSettings=false /p:TargetDatabase=UpdatedTargetDatabase '/p:TargetConnectionString=\"Data Source=(local)\SQLEXPRESS;Integrated Security=True;Pooling=False\"' "C:\program files\MyProjectName.dbproj"
Aprilette answered 24/6, 2011 at 15:58 Comment(1)
Almost, but this embeds the double quotes in the TargetConnectionString parameter, which will break the deployment step.Jacqulynjactation
J
1

Thanks to JohnF's answer, I was able to figure this one out, finally.

echoargs /target:clean`;build`;deploy /p:UseSandboxSettings=false /p:TargetConnectionString=`"Data
Source=.`;Integrated Security=True`;Pooling=False`" .\MyProj.dbproj
Arg 0 is </target:clean;build;deploy>
Arg 1 is </p:UseSandboxSettings=false>
Arg 2 is </p:TargetConnectionString=Data Source=.;Integrated Security=True;Pooling=False>
Arg 3 is <.\MyProj.dbproj>

In short, but backticks in front of the double quotes AND the semicolons. Anything less (or more!) will screw it up.

Jacqulynjactation answered 8/12, 2011 at 22:34 Comment(0)
G
1

It's mentioned in the Articles from this answer, but with PowerShell 3 you can use --% to stop the normal parsing PowerShell does.

msbuild --% /target:Deploy /p:UseSandboxSettings=false /p:TargetConnectionString="aConnectionWithSpacesAndSemiColons" "aDatabaseProjectPathWithSpaces"
Geld answered 4/5, 2013 at 19:58 Comment(2)
Sorry I missed it before, but after seeing your mention here and Mitul's recent investigation, I've added section 3.3 for this PSv3 feature :)Terenceterencio
@EmperorXLII: Great to see you improving your excelent answer. My I suggest that this is the easiest way (if you have v3) and might deserve a place higher up in your long version and also a mention in your short version?Geld

© 2022 - 2024 — McMap. All rights reserved.