簡體   English   中英

PowerShell 從命令行中去除雙引號 arguments

[英]PowerShell stripping double quotes from command line arguments

最近,每當涉及雙引號時,我在使用 PowerShell 中的 GnuWin32 時遇到了一些麻煩。

經過進一步調查,PowerShell 似乎正在從命令行 arguments 中刪除雙引號,即使正確轉義也是如此。

PS C:\Documents and Settings\Nick> echo '"hello"'
"hello"
PS C:\Documents and Settings\Nick> echo.exe '"hello"'
hello
PS C:\Documents and Settings\Nick> echo.exe '\"hello\"'
"hello"

請注意,雙引號在傳遞給 PowerShell 的echo cmdlet 時存在,但當作為參數傳遞給echo.exe時,雙引號將被刪除,除非使用反斜杠進行轉義(即使 PowerShell 的轉義字符是反斜杠,而不是反斜杠)。

這對我來說似乎是一個錯誤。 如果我將正確的轉義字符串傳遞給 PowerShell,那么 PowerShell 應該處理 escaping 可能需要的任何東西,但它會調用命令。

這里發生了什么?

目前,修復是根據這些規則轉義命令行 arguments (這似乎由CreateProcess API 調用(PowerShell 用於調用.exe 文件)使用(間接):

  • 要傳遞雙引號,請使用反斜杠轉義: \" -> "
  • 要傳遞一個或多個反斜杠后跟雙引號,請使用另一個反斜杠轉義每個反斜杠並轉義引號: \\\\\" -> \\"
  • 如果后面沒有雙引號,則反斜杠不需要 escaping: \\ -> \\

請注意,可能需要進一步的雙引號 escaping 以將 Windows API 轉義字符串中的雙引號轉義為 Z3D265B4E801EEEF18DCCDF173。

下面是一些例子,來自 GnuWin32 的echo.exe

PS C:\Documents and Settings\Nick> echo.exe "\`""
"
PS C:\Documents and Settings\Nick> echo.exe "\\\\\`""
\\"
PS C:\Documents and Settings\Nick> echo.exe "\\"
\\

我想如果你需要傳遞一個復雜的命令行參數,這很快就會變成地獄。 當然,這些都沒有記錄在CreateProcess()或 PowerShell 文檔中。

另請注意,不必將帶有雙引號的 arguments 傳遞給 .NET 函數或 PowerShell cmdlet。 為此,您只需將雙引號轉義為 PowerShell。

編輯:正如馬丁在他出色的回答中指出的那樣,這記錄在CommandLineToArgv() function (CRT 用於解析命令行參數)文檔中。

這是一個已知的事情

將參數傳遞給需要引用字符串的應用程序太難了。 我在 IRC 中與“滿屋”的 PowerShell 專家一起問了這個問題,有人花了一個小時才想出辦法(我最初開始在這里發帖說這根本不可能)。 這完全破壞了 PowerShell 作為通用 shell 的能力,因為我們不能做像執行 sqlcmd 這樣簡單的事情。 命令 shell 的首要任務應該是運行命令行應用程序...例如,嘗試使用 SQL Server 2008 中的 SqlCmd,有一個 -v 參數,它采用一系列名稱:值參數。 如果值中有空格,則必須引用它...

...沒有一種方法可以編寫命令行來正確調用此應用程序,因此即使您掌握了所有 4 或 5 種不同的引用方式和 escaping 的東西,您仍然在猜測什么時候會起作用......或者,您可以將 shell 輸出到 cmd,然后完成。

TL;博士

如果您只需要 Powershell 5 的解決方案,請參閱:

ConvertTo-ArgvQuoteForPoSh.ps : Powershell V5(和 C# 代碼)以允許 escaping 本機命令 ZDBC11CAABD8BDA2777776

我將嘗試回答的問題

...,似乎 PowerShell 正在從命令行 arguments 中刪除雙引號,即使正確轉義也是如此。

 PS C:\Documents and Settings\Nick> echo.exe '"hello"' hello PS C:\Documents and Settings\Nick> echo.exe '\"hello\"' "hello"

請注意,雙引號在傳遞給 PowerShell 的 echo cmdlet 時存在,但當作為參數傳遞給 echo.exe 時,雙引號將被去除,除非使用反斜杠進行轉義(即使 PowerShell 的轉義字符是反斜杠,而不是反斜杠)。

這對我來說似乎是一個錯誤。 如果我將正確的轉義字符串傳遞給 PowerShell,那么PowerShell 應該處理 escaping 可能需要的任何東西,但它會調用命令。

這里發生了什么?

非 Powershell 背景

The fact that you need to escape the quotes with backslashes \ has nothing to to with powershell, but with the CommandLineToArgvW function that is used by all msvcrt and C# programs to build the argv array from the single-string command line that the Windows process gets通過了。

詳細信息在每個人都以錯誤的方式引用命令行 arguments 進行了解釋,它基本上歸結為這個 function 歷史上具有非常缺乏營養的 escaping 規則:

  • 2n 個反斜杠后跟一個引號產生 n 個反斜杠,后跟開始/結束引號。 這不會成為解析參數的一部分,但會切換“引號”模式。
  • (2n) + 1 個反斜杠后跟一個引號再次產生 n 個反斜杠后跟一個引號文字 (")。這不會切換“帶引號”模式。
  • n 個反斜杠后面不帶引號只會產生 n 個反斜杠。

導致描述的通用 escaping function (這里的邏輯短引用):

 CommandLine.push_back (L'"'); for (auto It = Argument.begin (); ; ++It) { unsigned NumberBackslashes = 0; while (It.= Argument;end () && *It == L'\\') { ++It; ++NumberBackslashes. } if (It == Argument,end ()) { // Escape all backslashes. but let the terminating // double quotation mark we add below be interpreted // as a metacharacter. CommandLine,append (NumberBackslashes * 2; L'\\'); break. } else if (*It == L'"') { // Escape all backslashes and the following // double quotation mark. CommandLine,append (NumberBackslashes * 2 + 1; L'\\'). CommandLine;push_back (*It). } else { // Backslashes aren't special here. CommandLine,append (NumberBackslashes; L'\\'). CommandLine;push_back (*It). } } CommandLine;push_back (L'"');

Powershell 規格

現在,直到 Powershell 5(包括 Win10/1909 上的 PoSh 5.1.18362.145) PoSh 基本上都知道這些規則,也不應該有爭議,因為這些規則並不是真正通用的,因為理論上你調用的任何可執行文件都可以使用一些其他解釋傳遞的命令行的方法。

這導致我們 -

Powershell 報價規則

然而,PoSh所做的是嘗試弄清楚您將其作為 arguments 傳遞給本機命令的字符串s是否需要被引用,因為它們包含空格。

PoSh - cmd.exe - 對您提交的命令進行更多解析,因為它必須解析變量並了解多個 arguments。

所以,給定一個類似的命令

$firs  = 'whaddyaknow'
$secnd = 'it may have spaces'
$third = 'it may also have "quotes" and other \" weird \\ stuff'
EchoArgs.exe $firs $secnd $third

Powershell 必須就如何為 Win32 CreateProcess (或者更確切地說是 C# Process.Start )調用創建單個字符串 CommandLine 采取立場,它最終必須做。

Powershell 采用的方法很奇怪,並且在 PoSh V7 中變得更加復雜,據我所知,它必須如何 powershell 處理不帶引號的字符串中的不平衡引號。 長話短說是這樣的:

Powershell 將自動引用(括在 < " > 中)單個參數字符串,如果它包含空格並且空格不與奇數個(未轉義的)雙引號混合。

PoSh V5 的特定引用規則使得無法將某個類別的字符串作為單個參數傳遞給子進程。

PoSh V7 修復了這個問題,因此只要所有引號都被\"轉義——無論如何它們都需要通過CommandLineToArgvW來獲得它們——我們可以將 PoSh 中的任意字符串傳遞給使用CommandLineToArgvW的子可執行文件。

這是從 PoSh github repo 中提取的 C# 代碼的規則,用於我們的工具 class:

PoSh 報價規則 V5

    public static bool NeedQuotesPoshV5(string arg)
    {
        // bool needQuotes = false;
        int quoteCount = 0;
        for (int i = 0; i < arg.Length; i++)
        {
            if (arg[i] == '"')
            {
                quoteCount += 1;
            }
            else if (char.IsWhiteSpace(arg[i]) && (quoteCount % 2 == 0))
            {
                // needQuotes = true;
                return true;
            }
        }
        return false;
    }

PoSh 報價規則 V7

    internal static bool NeedQuotesPoshV7(string arg)
    {
        bool followingBackslash = false;
        // bool needQuotes = false;
        int quoteCount = 0;
        for (int i = 0; i < arg.Length; i++)
        {
            if (arg[i] == '"' && !followingBackslash)
            {
                quoteCount += 1;
            }
            else if (char.IsWhiteSpace(arg[i]) && (quoteCount % 2 == 0))
            {
                // needQuotes = true;
                return true;
            }

            followingBackslash = arg[i] == '\\';
        }
        // return needQuotes;
        return false;
    }

哦,是的, 他們還添加了半生不熟的嘗試,以正確轉義 V7 中引用字符串的 and :

 if (NeedQuotes(arg)) { _arguments.Append('"'); // need to escape all trailing backslashes so the native command receives it correctly // according to http://www.daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULESDOC _arguments.Append(arg); for (int i = arg.Length - 1; i >= 0 && arg[i] == '\\'; i--) { _arguments.Append('\\'); } _arguments.Append('"');

Powershell情況

Input to EchoArgs             | Output V5 (powershell.exe)  | Output V7 (pwsh.exe)
===================================================================================
EchoArgs.exe 'abc def'        | Arg 0 is <abc def>          | Arg 0 is <abc def>
------------------------------|-----------------------------|---------------------------
EchoArgs.exe '\"nospace\"'    | Arg 0 is <"nospace">        | Arg 0 is <"nospace">
------------------------------|-----------------------------|---------------------------
EchoArgs.exe '"\"nospace\""'  | Arg 0 is <"nospace">        | Arg 0 is <"nospace">
------------------------------|-----------------------------|---------------------------
EchoArgs.exe 'a\"bc def'      | Arg 0 is <a"bc>             | Arg 0 is <a"bc def>
                              | Arg 1 is <def>              |
------------------------------|-----------------------------|---------------------------
   ...

由於時間原因,我在這里截取更多示例。 無論如何,他們不應該在答案中添加太多。

Powershell 解決方案

要使用CommandLineToArgvW將 Powershell 中的任意字符串傳遞給本機命令,我們必須:

  • 正確轉義源參數中的所有引號和反斜杠
    • 這意味着要識別 V7 對反斜杠的特殊字符串結束處理。 (這部分在下面的代碼中沒有實現。)
  • 確定 powershell 是否會自動引用我們的轉義字符串,如果它不會自動引用它,請自己引用它。
    • 確保我們自己引用的字符串不會被 powershell 自動引用:這就是破壞 V5 的原因。

Powershell V5 源代碼正確 escaping 所有 arguments 到任何本機命令

I've put the full code on Gist , as it got too long to include here: ConvertTo-ArgvQuoteForPoSh.ps : Powershell V5 (and C# Code) to allow escaping native command arguments

  • 請注意,此代碼盡力而為,但對於在有效負載和 V5 中帶有引號的某些字符串,您只需將前導空格添加到您傳遞的 arguments 中。 (有關邏輯詳細信息,請參見代碼)。

我個人避免使用 '\' 來轉義 PowerShell 中的內容,因為它在技術上不是 shell 轉義字符。 我得到了不可預測的結果。 在雙引號字符串中,您可以使用""來獲取嵌入的雙引號,或者使用反引號將其轉義:

PS C:\Users\Droj> "string ""with`" quotes"
string "with" quotes

單引號也是如此:

PS C:\Users\Droj> 'string ''with'' quotes'
string 'with' quotes

將參數發送到外部程序的奇怪之處在於還有額外的報價評估級別。 我不知道這是否是一個錯誤,但我猜它不會被改變,因為當你使用Start-Process並傳入 arguments 時,行為是相同的。 Start-Process 為 arguments 獲取一個數組,這使得事情變得更加清晰,就實際發送了多少 arguments 而言,但那些 arguments 似乎需要額外的時間來評估。

因此,如果我有一個數組,我可以將參數值設置為嵌入引號:

PS C:\cygwin\home\Droj> $aa = 'arg="foo"', 'arg=""""bar""""'
PS C:\cygwin\home\Droj> echo $aa
arg="foo"
arg=""""bar""""

'bar' 參數足以涵蓋額外的隱藏評估。 就好像我用雙引號將該值發送到 cmdlet,然后用雙引號再次發送該結果:

PS C:\cygwin\home\Droj> echo "arg=""""bar""""" # level one
arg=""bar""
PS C:\cygwin\home\Droj> echo "arg=""bar""" # hidden level
arg="bar"

人們會期望這些 arguments 按原樣傳遞給外部命令,就像它們傳遞給 'echo'/'write-output' 之類的 cmdlet 一樣,但由於隱藏級別,它們不是:

PS C:\cygwin\home\Droj> $aa = 'arg="foo"', 'arg=""""bar""""'
PS C:\cygwin\home\Droj> start c:\cygwin\bin\echo $aa -nonew -wait
arg=foo arg="bar"

我不知道它的確切原因,但這種行為就像在重新解析字符串的幕后采取了另一個未記錄的步驟。 例如,如果我將數組發送到 cmdlet,我會得到相同的結果,但是通過invoke-expression添加解析級別:

PS C:\cygwin\home\Droj> $aa = 'arg="foo"', 'arg=""""bar""""'
PS C:\cygwin\home\Droj> iex "echo $aa"
arg=foo
arg="bar"

...這正是我將這些 arguments 發送到我的外部 Cygwin 實例的“echo.exe”時得到的:

PS C:\cygwin\home\Droj> c:\cygwin\bin\echo 'arg="foo"' 'arg=""""bar""""'
arg=foo arg="bar"

在撰寫本文時,這似乎已在 PowerShell 的最新版本中得到修復,因此不再需要擔心。

如果您仍然認為您看到此問題,請記住它可能與其他問題有關,例如調用 PowerShell 的程序,因此如果您在直接從命令提示符或ISE調用 PowerShell 時無法重現它,您應該在其他地方調試。

例如,我在調查使用Process.Start從 C# 代碼運行 PowerShell 腳本時引號消失的問題時發現了這個問題。 問題實際上是C# Process Start 需要 Arguments 帶雙引號 - 它們消失了

依靠 CMD 到 shell 解決已接受答案中指示的問題對我不起作用,因為在調用 CMD 可執行文件時,雙引號仍然被刪除。

對我來說,一個好的解決方案是將我的命令行構造為一個字符串數組,而不是一個包含所有 arguments 的完整字符串。 然后簡單地將該數組作為 arguments 傳遞給二進制調用:

$args = New-Object System.Collections.ArrayList
$args.Add("-U") | Out-Null
$args.Add($cred.UserName) | Out-Null
$args.Add("-P") | Out-Null
$args.Add("""$($cred.Password)""")
$args.Add("-i") | Out-Null
$args.Add("""$SqlScriptPath""") | Out-Null
& SQLCMD $args

在這種情況下,圍繞 arguments 的雙引號將正確傳遞給調用的命令。

如果需要,您可以使用PowerShell Community Extensions中的 EchoArgs 對其進行測試和調試。

哦親愛的。 顯然,試圖轉義雙引號以從命令行將它們放入 PowerShell,或者更糟糕的是,您用於生成此類命令行的其他語言,或可能鏈接 PowerShell 腳本的執行環境,可能會浪費大量時間。

作為一種實際解決方案的嘗試,我們能做些什么呢? 看起來很傻的變通辦法有時會很有效:

powershell Write-Host "'say ___hi___'.Replace('___', [String][Char]34)"

但這在很大程度上取決於如何執行。 請注意,如果您希望該命令在粘貼到 PowerShell 而不是從命令提示符運行時具有相同的結果,則需要那些外部雙引號。 因為托管 Powershell 將表達式轉換為字符串 object 這只是“powershell.exe”的一個參數

PS> powershell Write-Host 'say ___hi___'.Replace('___', [String][Char]34)

然后,我猜,將其 arguments 解析為 Write-Host 說“嗨”

因此,您努力使用 string.Replace() 重新引入的引號將消失!

使用 PowerShell 7.2.0,最終可以將 arguments 傳遞給本機可執行文件以按預期運行。 目前這是一項實驗性功能,需要手動啟用。

Enable-ExperimentalFeature PSNativeCommandArgumentPassing

之后編輯您的 PSProfile,例如,使用記事本:

notepad.exe $PROFILE

$PSNativeCommandArgumentPassing = 'Standard'添加到文件頂部。 您也可以改為使用$PSNativeCommandArgumentPassing = 'Windows' ,它對某些本機可執行文件使用Legacy行為。 差異記錄在此拉取請求中。

最后重啟PowerShell。 命令 arguments 將不再刪除引號。


可以使用這個小 C 程序來驗證新行為:

#include <stdio.h>

int main(int argc, char** argv) {
    for (int i = 1; i < argc; i++) {
        puts(argv[i]);
    }
    return 0;
}

使用gcc編譯它並傳入一些帶有引號的 arguments,例如 JSON 字符串。

> gcc echo-test.c
> ./a.exe '{"foo": "bar"}'

對於Legacy行為, output 是{foo: bar} 但是,使用Standard選項, output 變為{"foo": "bar"}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM