使用Powershell调用带有嵌套引号的msbuild

26
使用 Powershell 和 Psake 创建一个包并部署 Visual Studio 解决方案。尝试使用 MSBuild 部署数据库项目 - 在使用 MSDOS Visual Studio 命令行时已经正确工作。
   msbuild /target:Deploy /p:UseSandboxSettings=false /p:TargetConnectionString="aConnectionWithSpacesAndSemiColons" "aDatabaseProjectPathWithSpaces"

当从 PowerShell 调用相同的方法时,会导致错误。

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

与空格有关 - 我无法想出如何在PowerShell中复制此调用 - 示例数据库连接字符串 Data Source=.\SQL2008;Initial Catalog=DocumentExecution;Integrated Security=True;

7个回答

62

简短版本

如何从PowerShell将包含引号的参数传递给本地命令?

  • 在参数字符串中使用单引号而不是双引号:
       "/p:Target='Data Source=(local)\SQL;Integrated Security=True'"
    /p:Target='Data Source=(local)\SQL;Integrated Security=True'

  • 在参数字符串中使用反斜杠转义双引号
       '/p:Target=\"Data Source=(local)\SQL;Integrated Security=True\"'
    /p:Target="Data Source=(local)\SQL;Integrated Security=True"

如果嵌入的引号仅用于将参数视为单个字符串,而不是参数的必需部分,则可以使用以下方法:

  • 引用整个参数字符串,而不是在参数中嵌入引号:
       /p:Target=Data Source=(local)\SQL;Integrated Security=True

  • 使用反引号转义所有PowerShell特殊字符(只能在内联参数中完成):
       /p:Target=`"Data Source=`(local`)`\SQL`;Integrated Security=True`"
    或者/p:Target=Data` Source=`(local`)`\SQL`;Integrated` Security=True
    /p:Target=Data` Source=`(local`)`\SQL`;Integrated` Security=True


完整的命令行示例(使用第二种选择):

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
参数 0 是 </target:Deploy>
参数 1 是 </p:UseSandboxSettings=False>
参数 2 是 </p:TargetDatabase=UpdatedTargetDatabase>
参数 3 是 </p:TargetConnectionString="Data Source=(local)\SQL;Integrate Security=True">
参数 4 是 <C:\program files\MyProjectName.dbproj>



长版

在从传统的cmd系统转换到PowerShell时,调用本地命令是经常出现的事情(几乎和“用逗号分隔参数”一样多)。

我已经尽力总结了我所知道的有关PowerShell(v2和v3)中命令调用的所有内容,并提供了所有我能想到的例子和参考资料。


1) 直接调用本地命令

1.1) 最简单的情况是,如果一个可执行文件位于环境路径中,那么就可以直接调用该命令,就像调用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) 在环境路径之外,可以使用完整或相对路径执行特定目录中(包括当前目录中)的命令。其思想是让操作员明确声明“我想调用这个文件”,而不是让任意具有相同名称的文件运行在其原本的位置上(有关PowerShell安全性的更多信息,请参见此问题)。在需要使用路径时未使用路径将导致“术语未被识别”错误。

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

如果路径包含特殊字符,可以使用调用运算符或转义字符。例如,以数字开头的可执行文件,或位于包含空格的目录中。
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) 间接调用本地命令

2.1) 当您不是交互式地输入命令,而是将路径存储在变量中时,调用运算符也可以用于调用存储在变量中的命令

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) 命令传递的参数也可以存储在变量中。 变量中的参数可以逐个传递,也可以作为数组传递。 对于包含空格的变量,PowerShell会自动转义空格,以便原生命令将其视为单个参数。(请注意,调用运算符将第一个值视为命令,将剩余的值视为参数;参数不应与命令变量组合。)

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) 数组格式对于构建本地命令的动态参数列表特别有用。 为了使每个参数被识别为单独的参数,将参数存储在数组变量中非常重要,而不是将它们混合在一个字符串中。(请注意,常见的缩写$args是PowerShell中的自动变量,这可能会导致保存在其中的值被覆盖;因此,最好使用描述性名称如$msbuildArgs来避免命名冲突。)

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) 另外,对于脚本、函数、cmdlet等,PowerShell v2可以使用一种称为“splatting”的技术发送包含命名参数的哈希表,而无需担心参数顺序。但是,这对于不参与PowerShell对象模型并且只能处理字符串值的本机命令无效。

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) 调用带有复杂参数的本地命令

3.1) 对于简单的参数,通常使用本地命令的自动转义就足够了。但是,对于括号、美元符号、空格等 PowerShell 使用的字符需要进行转义才能原样发送给本地命令,而不是被解释器解释。可以使用反引号转义字符`或将参数放在单引号字符串中来实现此操作。

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) 当涉及到双引号时,情况就不那么简单了。作为本机命令的参数处理的一部分,PowerShell处理器尝试规范化参数中的所有双引号,以便将参数内容(不带引号)作为单个值传递给本机命令。本机命令参数处理发生在解析之后的单独步骤中,因此普通的转义对于双引号不起作用;只能使用转义的单引号或反斜杠转义的双引号
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增加了一个新的停止解析符号--%(参见about_Parsing)。当在复杂参数之前使用时,--%将原样传递参数,而不进行任何解析或变量扩展,除了类似cmd的%ENVIRONMENT_VARIABLE%值。

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>

这也可以用来解开一个表示多个参数的字符串,只需将停止解析符号作为字符串传递即可(尽管最好的做法是一开始就不要混淆参数)。
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) 调试本机命令

有两个关键工具可以调试PowerShell传递给本机命令的参数。

4.1) 第一个是EchoArgs.exe,它是PowerShell社区扩展中的控制台应用程序,只需将传递给它的参数写回尖括号之间(如上面的示例所示)。

4.2) 第二个是Trace-Command,这是一个可显示PowerShell处理管道的许多细节的cmdlet。特别地,NativeCommandParameterBinder跟踪源将显示PowerShell接收并传递给本机命令的内容。

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"


其他资源

文章

问题


2
http://rkeithhill.wordpress.com/2012/01/02/powershell-v3-ctp2-provides-better-argument-passing-to-exes/ - Ruben Bartelink
1
我在StackOverflow上看到的最好、最完整的答案之一! - Sharpenologist

11

如果您使用带有-ArgumentList参数的Start-Process cmdlet,所有这些都可以变得更加容易。我很惊讶这还没有被提到。

示例:

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

这是我比较喜欢使用的一种方法,它允许进行变量替换:

$ConnectionString = 'aConnectionWithSpacesAndSemiColons';
$DatabaseProjectPath = 'aDatabaseProjectPathWithSpaces';
$MsbuildArguments = '/target:Deploy /p:UseSandboxSettings=false /p:TargetConnectionString="{0}" "{1}"' -f $ConnectionString, $DatabaseProjectPath;
Start-Process -FilePath msbuild.exe -ArgumentList $MsbuildArguments;

4

将整个参数放在单引号中:

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

额外的引用级别意味着PSH不会使用PSH规则处理内容。在字符串内部,任何单引号都需要加倍——这是PSH单引号字符串中唯一的转义方式。

1
这个答案的文章中提到过,但使用PowerShell 3,您可以使用--%来停止PowerShell执行常规解析。
msbuild --% /target:Deploy /p:UseSandboxSettings=false /p:TargetConnectionString="aConnectionWithSpacesAndSemiColons" "aDatabaseProjectPathWithSpaces"

抱歉之前错过了,但在看到你在这里的提及和Mitul最近的调查之后,我已经为此PSv3功能添加了第3.3节 :) - Emperor XLII
@EmperorXLII:很高兴看到您在改进您出色的答案。我可以建议,如果您有v3,这是最简单的方法,并且可能值得在您的长版本中更高的位置以及在您的短版本中提及? - Lars Truijens

1

你的问题在于PowerShell在将引号传递给命令行应用程序时不会对其进行转义。我自己也遇到过这个问题,以为是PowerShell吃掉了引号。只需执行以下操作即可解决。

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"

几乎可以了,但是这会将双引号嵌入到TargetConnectionString参数中,从而破坏部署步骤。 - moswald

1

@Richard - 测试结果显示出现了不同的错误,提示未提供有效的项目文件。 我已经通过echoargs pscx助手运行了此程序,以展示更详细的示例。

  • 使用单引号包裹TargetConnectionString - Powershell会将连接字符串中的每个空格作为新行进行评估:

    & 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>
    
  • 使用反引号分隔每个参数会重新创建初始问题 = 连接字符串周围没有引号:

    & 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>
    
  • 在引号中添加反引号的行为与示例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"
    
  • 使用@运算符尝试拆分参数仍然忽略引号:

    $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
    
  • 使用反引号转义连接字符串使用换行符 - 与示例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,取得了成功,并在我的机器上进行了快速测试以保留引号。在PowerShell中,变量$ args具有特殊含义;您可以尝试使用不同的名称,例如$ msbuildArgs。 - Emperor XLII

    1
    感谢JohnF的回答,我终于能够解决这个问题了。
    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>
    

    简而言之,在双引号和分号前面加上反引号。任何少于或多于这个规则的操作都会导致出错。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接