source

PowerShell이 명령줄 인수에서 큰따옴표를 제거합니다.

ittop 2023. 10. 4. 22:57
반응형

PowerShell이 명령줄 인수에서 큰따옴표를 제거합니다.

최근 저는 큰따옴표가 관련될 때마다 파워쉘의 GnuWin32를 사용하는 데 문제가 생겼습니다.

추가 조사 결과 PowerShell이 적절하게 탈출한 경우에도 명령줄 인수에서 큰따옴표를 제거하는 것으로 나타났습니다.

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은 명령을 실행하는 데 필요한 모든 이스케이프를 처리해야 합니다.

이게 무슨 일입니까?

를(으로) (되는 것으로 입니다.CreateProcessPowerShell이 .exe 파일을 호출하는 데 사용하는 API 호출:

  • ( 합니다)를 \"->"
  • 하나 두 각 백슬래시로 하고 따옴표인 합니다를(를) \\\\\"->\\"
  • 백슬래시) 에(백슬래시) 를(를) 가 없습니다.\\->\\

Windows API 이스케이프 문자열에서 PowerShell로 이중 따옴표를 탈출하려면 이중 따옴표를 더 탈출해야 할 수도 있습니다.

다음은 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 설명서를 참조할 수 있습니다.

큰따옴표가 있는 인수를 에 전달할 필요는 없습니다.NET 기능 또는 PowerShell cmdlet.이를 위해서는 PowerShell에 큰따옴표를 달아나기만 하면 됩니다.

편집: Martin이 그의 훌륭한 답변에서 지적했듯이, 이것은 문서에 기록되어 있습니다.CommandLineToArgv()함수(CRT가 명령줄 인수를 구문 분석하는 데 사용함) 설명서.

이것은 알려진 것입니다.

따옴표로 묶은 문자열이 필요한 응용프로그램에 매개변수를 전달하는 것은 너무 어렵습니다.IRC에서 PowerShell 전문가로 구성된 "방이 꽉 찬" 사람들과 함께 이 질문을 던졌는데, 누군가 방법을 알아내기 위해 몇 시간이 걸렸습니다(원래는 불가능하다는 글을 여기에 올리기 시작했습니다).이렇게 하면 PowerShell이 범용 셸 역할을 수행하는 기능이 완전히 상실됩니다. sqlcmd를 실행하는 것과 같은 간단한 작업을 수행할 수 없기 때문입니다.명령 셸의 첫 번째 작업은 명령줄 응용 프로그램을 실행하는 것이어야 합니다.예를 들어 SQL Server 2008의 SqlCmd를 사용하려고 하면 일련의 name:value 매개 변수를 사용하는 -v 매개 변수가 있습니다.값에 공백이 있으면 따옴표를 붙여야 합니다.

...이 응용 프로그램을 올바르게 실행하기 위한 명령행을 작성하는 방법은 하나도 없습니다. 따라서 4~5가지 인용 및 탈출 방법을 모두 숙달한 후에도 다음과 같은 경우에 어떤 방법이 효과가 있을지 짐작할 수 있습니다.아니면 그냥 cmd에 셸아웃해서 끝내도 됩니다.

TL;DR

Powershell 5에 대한 솔루션을 원하는 경우 다음을 참조하십시오.

ConvertTo-ArgvQuoteForPoSh.ps 하는 Powershell V5(C# Code )

제가 대답해 드리려 하는 질문입니다.

..., PowerShell이 적절하게 이스케이프된 경우에도 명령줄 인수에서 큰따옴표를 제거하는 것으로 보입니다.

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은 명령을 실행하는 데 필요한 모든 이스케이프를 처리해야 합니다.

이게 무슨 일입니까?

비파워셸 배경

\파워셸과는 아무 관련이 없지만, 모든 msvcrt 및 C# 프로그램이 구축하는 데 사용하는 기능과.argv윈도우즈 프로세스가 전달되는 단일 문자열 명령줄에서 배열합니다.

자세한 내용은 Everyone quotes 명령줄 인수에서 잘못된 방식으로 설명되며, 기본적으로 이 함수가 역사적으로 매우 비효율적인 탈출 규칙을 가지고 있다는 사실로 요약됩니다.

  • 2n개의 백슬래시 뒤에 따옴표가 붙으면 n개의 백슬래시가 생성되고 시작/종료 따옴표가 이어집니다.구문 분석된 인수의 일부가 되지 않고 "따옴표로" 모드를 전환합니다.
  • (2n) + 백슬래시 1개 다음에 따옴표가 다시 n개의 백슬래시를 생성하고 따옴표가 리터럴(")로 이어집니다.따옴표 안의 모드는 전환하지 않습니다.
  • 따옴표가 뒤따르지 않는 n개의 백슬래시는 단순히 n개의 백슬래시를 생성합니다.

설명된 일반 탈출 함수로 이어집니다(여기 논리의 짧은 인용문).

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 5까지(Win10/1909의 PoSh 5.1.18362.145 포함) PoSh는 기본적으로 이러한 규칙에 대해 정확히 알고 있으며, 이론적으로 이러한 규칙이 일반적이지 않기 때문에, 호출하는 모든 실행 파일은 전달된 명령줄을 해석하기 위해 다른 수단을 사용할 수 있습니다.

그래서 우리는..

파워셸 인용 규칙

그러나 PoSh는 native 명령에 인수로 전달하는 문자열이 공백을 포함하고 있으므로 따옴표로 사용할 필요가 있는지 여부를 파악합니다.

PoSH는 이와 대조적으로 변수를 해결해야 하고 여러 인수에 대해 알고 있기 때문에 전달하는 명령에 대해 훨씬 더 많은 구문 분석을 수행합니다.

그래서, 그와 같은 명령이 주어집니다.

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

파워셸은 Win32를 위한 단일 문자열 CommandLine을 만드는 방법에 대해 입장을 취해야 합니다.CreateProcess려 C#)Process.Start전화를 걸어봐야 할 것입니다.

PoSh V7에서 파워셸이 취하는 접근 방식은 이상하고 더 복잡해졌습니다. 그리고 제가 따를 수 있는 한, 파워셸이 따옴표가 없는 문자열에서 불균형한 따옴표를 다루는 방법을 수행해야 합니다.간단히 말하면 다음과 같습니다.

합니다(< 합니다(<))에 ">) 하나의 인수 문자열. 공백이 포함되어 있고 공백이 짝수 개의 (탈출하지 않은) 큰따옴표와 섞이지 않는 경우.

PoSH V5의 특정 인용 규칙은 자식 프로세스에 특정 범주의 문자열을 단일 인수로 전달하는 것을 불가능하게 합니다.

은 모든 이 PoSh V7 인 했습니다.\"탈출한 사람들이 그들을 통과시키기 위해서는 어떻게든 필요한 사람들입니다.CommandLineToArgvW에서 PoSH다를 실행 로 임의의 할 수 .CommandLineToArgvW.

다음은 우리의 툴 클래스에 대한 PoS github repo에서 추출된 C# 코드로서의 규칙입니다.

포쉬 인용 규칙 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;
    }

포쉬 인용 규칙 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에서 인용된 문자열의 정확한 탈출을 위해 어설픈 시도를 추가했습니다.

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('"');

파워셸 상황

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, 다음을 수행해야 합니다.

  • source 인수의 모든 따옴표와 백슬래시를 적절히 이스케이프합니다.
    • 이것은 V7이 가지고 있는 백슬래시에 대한 특별한 문자열 끝 처리를 인식하는 것을 의미합니다. (이 부분은 아래 코드에서 구현되지 않습니다.)
  • 파워셸이 탈출한 문자열을 자동으로 quote할지 결정하고 자동으로 quote하지 않으면 직접 인용합니다.
    • 우리가 직접 인용한 문자열이 파워셸에 의해 자동 quoted되지 않도록 해야 합니다.이것이 V5를 깨뜨리는 것입니다.

모든 인수를 네이티브 명령으로 올바르게 탈출하기 위한 Powershell V5 소스 코드

여기에 포함하기에 시간이 너무 오래 걸려서 Gist에 전체 코드를 넣었습니다. : 네이티브 명령 인수에서 벗어날있도록 파워셸 V5(및 C# 코드)

  • 이 코드는 최선을 시도하지만 페이로드 및 V5에 따옴표가 있는 일부 문자열의 경우 전달하는 인수에 선행 공간을 추가해야 합니다.(논리에 대한 자세한 내용은 코드 참조).

저는 개인적으로 '\'을 사용하여 파워쉘에서 탈출하는 것을 피합니다. 왜냐하면 그것은 엄밀히 말하면 쉘 탈출 캐릭터가 아니기 때문입니다.저는 그것으로 예측할 수 없는 결과를 얻었습니다.할 수 있습니다""내장된 이중 quote을 얻거나 백 tick로 탈출하는 방법:

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

따옴표 하나도 마찬가지입니다.

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

파라미터를 외부 프로그램에 보내는 것의 이상한 점은 견적 평가의 추가 수준이 있다는 것입니다.이것이 버그인지는 모르겠지만, Start-Process를 사용하고 인수를 전달할 때 동작이 동일하기 때문에 변경되지는 않을 것으로 추측됩니다.Start-Process는 인수 배열을 사용하여 실제로 전송되는 인수의 수에 따라 상황을 좀 더 명확하게 만들지만, 그러한 인수는 추가 시간으로 평가되는 것 같습니다.

배열이 있는 경우 인수 값에 포함된 따옴표를 사용하도록 설정할 수 있습니다.

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"

이러한 인수는 '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"

...이러한 주장을 외부 Cygwin 인스턴스의 'echo.exe'로 전송하면 정확히 알 수 있습니다.

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

PowerShell 7.2.0을 사용하면 네이티브 실행 파일로 전달된 인수가 예상대로 동작할 수 있습니다.이 기능은 현재 실험 기능이므로 수동으로 활성화해야 합니다.

Enable-ExperimentalFeature PSNativeCommandArgumentPassing

그 후 메모장을 사용하여 PSProfile을 편집합니다.

notepad.exe $PROFILE

더하다$PSNativeCommandArgumentPassing = 'Standard'파일의 맨 위까지.다를 도 있습니다.$PSNativeCommandArgumentPassing = 'Windows'하는.Legacy일부 네이티브 실행 파일에 대한 동작입니다.차이점은 이 꺼내기 요청에 기록되어 있습니다.

마지막으로 PowerShell을 다시 시작합니다.명령 인수는 더 이상 따옴표를 제거하지 않습니다.


이 작은 C 프로그램으로 새로운 동작을 확인할 수 있습니다.

#include <stdio.h>

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

다음과 함께 컴파일합니다.gccJSON 문자열처럼 따옴표를 사용하여 인수를 전달합니다.

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

Legacy작,작{foo: bar}..Standard,,{"foo": "bar"}.

CMD 실행 파일을 호출할 때 이중 따옴표가 여전히 제거되었기 때문에, 수락된 답변에 나와 있는 문제를 CMD가 밀어내는 것에 의존하는 것은 저에게는 통하지 않았습니다.

제게 좋은 해결책은 명령행을 모든 인수를 포함하는 하나의 전체 문자열이 아닌 문자열의 배열로 구성하는 것이었습니다.그런 다음 해당 배열을 이진 호출의 인수로 간단히 전달합니다.

$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

이 경우 인수를 둘러싼 큰따옴표가 호출된 명령에 적절하게 전달됩니다.

필요한 경우 PowerShell Community Extensions의 EchoArgs를 사용하여 테스트하고 디버깅할 수 있습니다.

맙소사.명령줄에서 PowerShell로 이동하기 위해 이중 따옴표를 사용하지 않거나 더 나쁜 경우에는 해당 명령줄을 생성하는 데 사용하는 다른 언어나 PowerShell 스크립트를 체인으로 연결할 수 있는 실행 환경에서 큰 시간 낭비가 될 수 있습니다.

실용적인 해결책으로, 우리가 대신 할 수 있는 것은 무엇입니까?어리석어 보이는 해결책은 때때로 효과적일 수 있습니다.

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

그러나 이것이 어떻게 실행되느냐에 따라 많은 것이 달라집니다.명령 프롬프트에서 실행하는 대신 PowerShell에 붙여넣었을 때 해당 명령어의 결과가 동일하게 나오도록 하려면 이러한 바깥쪽 큰따옴표가 필요합니다!왜냐하면 호스팅 파워셸은 표현식을 문자열 객체로 전환하고 이는 단지 'powershell.exe'의 매개변수가 되기 때문입니다.

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

그러면 Write-Host가 "안녕"이라고 말하는 것처럼 그 주장을 해석하는 것 같습니다.

그래서 여러분이 열심히 문자열로 다시 소개하려고 하는 인용문.바꾸기()가 사라집니다!

이 글을 쓸 당시 PowerShell의 최신 버전에서는 이 문제가 해결된 것으로 보이므로 더 이상 걱정할 필요가 없습니다.

그래도 이 문제가 나타난다고 생각되면 PowerShell을 호출하는 프로그램과 같은 다른 것과 관련이 있을 수 있으므로 명령 프롬프트나 ISE에서 PowerShell을 직접 호출할 때 이 문제를 재생할 수 없다면 다른 곳에서 디버깅해야 합니다.

를 들어, , C# 에서 PowerShell 했습니다를 때할 때 이 했습니다.Process.Start. 문제는 사실 C# Process Start needs Arguments 중 큰 따옴표가 있는 것으로 사라집니다.

언급URL : https://stackoverflow.com/questions/6714165/powershell-stripping-double-quotes-from-command-line-arguments

반응형