PowerShell извлекает двойные кавычки из аргументов командной строки

В последнее время у меня возникали некоторые проблемы с использованием GnuWin32 из PowerShell при использовании двойных кавычек.

После дальнейшего изучения кажется, что 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"

Обратите внимание, что двойные кавычки присутствуют при передаче в командлет echo PowerShell, но когда они передаются в качестве аргумента echo.exe, двойные кавычки удаляются, если их не экранируют обратной косой чертой (даже если экранирующий символ PowerShell является обратным слэком, а не обратной косой чертой).

Это похоже на ошибку для меня. Если я передаю правильные экранированные строки в PowerShell, тогда PowerShell должен позаботиться о том, что экранирование может понадобиться, однако при этом она вызывает команду.

Что здесь происходит?

На данный момент исправление состоит в том, чтобы экранировать аргументы командной строки в соответствии с этими правилами (которые, похоже, используются CreateProcess Вызов API, который PowerShell использует для вызова файлов.exe):

  • Чтобы передать двойную кавычку, используйте обратную косую черту: \" -> "
  • Чтобы передать одну или несколько обратных косых черт, за которыми следует двойная кавычка, избегайте каждой обратной косой черты с другой обратной косой чертой и избегайте кавычки: \\\\\" -> \\"
  • Если за ним не следует двойная кавычка, для обратной косой черты нет необходимости: \\ -> \\

Обратите внимание, что дальнейшее экранирование двойных кавычек может потребоваться для экранирования двойных кавычек в экранированной строке Windows API в PowerShell.

Вот несколько примеров с echo.exe из GnuWin32:

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. Для этого вам нужно только избежать двойных кавычек в PowerShell.

8 ответов

Решение

Это известная вещь:

Слишком сложно передать параметры приложениям, которым требуются строки в кавычках. Я задал этот вопрос в IRC "просторным" экспертам PowerShell, и кому-то понадобился час, чтобы найти способ (я изначально начал публиковать здесь, что это просто невозможно). Это полностью нарушает способность PowerShell служить оболочкой общего назначения, потому что мы не можем делать простые вещи, такие как выполнение sqlcmd. Задача номер один в командной оболочке должна запускать приложения командной строки... Например, при попытке использовать SqlCmd из SQL Server 2008 существует параметр -v, который принимает ряд параметров name:value. Если в значении есть пробелы, вы должны заключить его в кавычки...

... не существует единственного способа написать командную строку для правильного вызова этого приложения, поэтому даже после того, как вы освоите все 4 или 5 различных способов цитирования и экранирования, вы все равно будете догадываться, какой будет работать, когда... или вы можете просто раскошелиться на cmd и покончить с этим.

TL;DR

Если вам просто нужно решение для Powershell 5, см.

ConvertTo-ArgvQuoteForPoSh.ps: Powershell V5 (и код C#), позволяющий экранировать аргументы собственных команд

Вопрос, на который я постараюсь ответить

..., похоже, PowerShell удаляет двойные кавычки из аргументов командной строки, даже если они правильно экранированы.

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

Обратите внимание, что двойные кавычки присутствуют при передаче в командлет PowerShell echo, но при передаче в качестве аргумента в echo.exe двойные кавычки удаляются, если они не экранированы обратной косой чертой (даже если escape-символ PowerShell является обратным знаком, а не обратной косой чертой).

Мне это кажется ошибкой. Если я передаю в PowerShell правильные экранированные строки, то PowerShell должен позаботиться обо всем, что может потребоваться экранирование, тем не менее, он вызывает команду.

Что здесь происходит?

Предпосылки создания не Powershell

Тот факт, что вам нужно избегать кавычек с помощью обратной косой черты \не имеет ничего общего с PowerShell, но с CommandLineToArgvW функция, которая используется всеми программами msvcrt и C# для создания argv массив из однострочной командной строки, передаваемой процессу Windows.

Подробности объясняются на странице Все неправильно цитируют аргументы командной строки, и в основном это сводится к тому факту, что эта функция исторически имеет очень неясные правила экранирования:

  • 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

Теперь, вплоть до Powershell 5 (включая PoSh 5.1.18362.145 на Win10/1909), PoSh в основном знает об этих правилах, и это не должно быть спорным, потому что эти правила не совсем общие, потому что любой исполняемый файл, который вы вызываете, теоретически может использовать некоторые другие средства интерпретации переданной командной строки.

Что приводит нас к -

Правила цитирования Powershell

Что Posh делает делать, однако, попытаться выяснить, является ли строка S вы передаете его в качестве аргументов нативные команды должны быть заключены в кавычки, потому что они содержат пробелы.

PoSh - в отличие отcmd.exe - выполняет намного больше синтаксического анализа команды, которую вы ей передаете, поскольку он должен разрешать переменные и знает о нескольких аргументах.

Итак, учитывая команду вроде

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

Powershell должен принять решение о создании однострочной CommandLine для Win32.CreateProcess (а точнее C# Process.Start) звоните, это всегда придется делать.

Подход Powershell странный и усложнился в PoSh V7, и, насколько я могу понять, он должен делать то, как powershell обрабатывает несбалансированные кавычки в строке без кавычек. Вкратце длинная история такова:

Powershell будет автоматически цитировать (заключить в <">) строка с одним аргументом, если она содержит пробелы и пробелы не смешиваются с нечетным количеством (без ограничения) двойных кавычек.

Особые правила цитирования PoSh V5 делают невозможным передачу определенной категории строк в качестве единственного аргумента дочернему процессу.

PoSh V7 исправил это, так что пока все кавычки \" сбежали - что им нужно в любом случае, чтобы пройти через CommandLineToArgvW - мы можем передать любую произвольную строку из PoSh в дочерний исполняемый файл, который использует CommandLineToArgvW.

Вот правила в виде кода C#, извлеченного из репозитория PoSh на github для нашего класса инструментов:

Правила цитирования PoSh, версия 5

    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:

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

Чтобы передать произвольные строки из Powershell в собственную команду, используя CommandLineToArgvW, мы должны:

  • правильно экранировать все кавычки и обратную косую черту в исходном аргументе
    • Это означает признание специальной обработки конца строки для обратной косой черты, которая есть в V7. (Эта часть не реализована в приведенном ниже коде.)
  • и определить, будет ли PowerShell автоматически заключать в кавычки нашу экранированную строку, и, если она не будет автоматически заключать ее в кавычки, цитировать сами.
    • и убедитесь, что строка, которую мы сами процитировали, не цитируется автоматически Powershell: это то, что нарушает V5.

Исходный код Powershell V5 для правильного экранирования всех аргументов любой собственной команды

Я разместил полный код на Gist, так как он слишком длинный, чтобы включать его сюда: ConvertTo-ArgvQuoteForPoSh.ps: Powershell V5 (и код C#), позволяющий экранировать аргументы собственных команд

  • Обратите внимание, что этот код старается изо всех сил, но для некоторых строк с кавычками в полезной нагрузке и V5 вы просто должны добавить начальный пробел к передаваемым аргументам. (См. Код для деталей логики).

Я лично избегаю использования "\" для экранирования вещей в PowerShell, потому что технически это не экранирующий символ оболочки. Я получил непредсказуемые результаты с ним. В двойных кавычках вы можете использовать "" чтобы получить встроенную двойную кавычку или убрать ее с помощью обратной галочки:

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

То же самое касается одинарных кавычек:

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

Странная вещь при отправке параметров во внешние программы заключается в том, что существует дополнительный уровень оценки котировок. Я не знаю, является ли это ошибкой, но я предполагаю, что она не изменится, потому что поведение при запуске Start-Process и передаче аргументов одинаково. Start-Process принимает массив для аргументов, что делает вещи более понятными с точки зрения того, сколько аргументов фактически отправляется, но эти аргументы, кажется, оцениваются в дополнительное время.

Итак, если у меня есть массив, я могу установить в значениях arg кавычки:

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

Аргумента 'bar' достаточно, чтобы покрыть дополнительную скрытую оценку. Это как если бы я отправил это значение командлету в двойных кавычках, а затем снова отправил этот результат в двойных кавычках:

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", но это не так, из-за этого скрытого уровня:

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"

Я не знаю точной причины этого, но поведение таково, как будто есть другой, недокументированный шаг, предпринимаемый под покрытиями, который повторно анализирует строки. Например, я получаю тот же результат, если отправляю массив в командлет, но добавляю уровень синтаксического анализа, выполняя его через 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'который использует поведение для некоторых собственных исполняемых файлов. Различия задокументированы в этом запросе на включение .

Наконец, перезапустите PowerShell. Аргументы команды больше не будут иметь удаленных кавычек.


Новое поведение можно проверить с помощью этой небольшой программы на C:

      #include <stdio.h>

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

Скомпилируйте его с gccи передайте некоторые аргументы в кавычках, например строку JSON.

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

С Legacyповедение, вывод {foo: bar}. Однако с Standardвариант, вывод становится {"foo": "bar"}.

Кажется, это было исправлено в последних версиях PowerShell на момент написания этой статьи, поэтому больше не о чем беспокоиться.

Если вы все еще думаете, что видите эту проблему, помните, что она может быть связана с чем-то другим, например, с программой, которая вызывает PowerShell, поэтому, если вы не можете воспроизвести ее при вызове PowerShell непосредственно из командной строки или ISE, вам следует выполнить отладку в другом месте.

Например, я обнаружил этот вопрос при исследовании проблемы исчезновения кавычек при запуске скрипта PowerShell из кода C# с использованием Process.Start, Проблема была на самом деле в C# Process Start нужны аргументы с двойными кавычками - они исчезают

Полагаться на 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

В этом случае двойные кавычки, окружающие аргументы, правильно передаются вызываемой команде.

Если вам нужно, вы можете протестировать и отладить его с помощью EchoArgs из расширений сообщества PowerShell.

О, Боже. Ясно, что попытки избежать двойных кавычек, чтобы перенести их в PowerShell из командной строки или, что еще хуже, на каком-либо другом языке, который вы используете для создания такой командной строки, или средах выполнения, которые могут связывать сценарии PowerShell, могут быть колоссальной тратой времени.

Что мы можем сделать вместо этого в качестве попытки практического решения? Иногда могут быть эффективны глупо выглядящие обходные пути:

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

Но это во многом зависит от того, как это выполняется. Обратите внимание: если вы хотите, чтобы эта команда давала те же результаты при вставке в PowerShell, а не запускалась из командной строки, вам нужны эти внешние двойные кавычки! Поскольку хост-сервер Powershell превращает выражение в строковый объект, который становится еще одним параметром для powershell.exe

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

Которая затем, я думаю, анализирует свои аргументы, когда Write-Host говорит "привет"

Таким образом, кавычки, которые вы так пытаетесь повторно ввести с помощью string.Replace(), просто исчезнут!

Вы можете решить проблему поведения PowerShell, утроив все двойные кавычки:

PS> .\echo.exe '"""help"""'
"help"
PS> .\echo.exe '"""help""""""'
"help""

Это поведение отчасти задокументировано в ProcessStartInfo.Argument,

Это работает для исполняемых файлов, которые не делают ничего смешного с самими аргументами. Кто-то конкретный пример sqlcmd -v Аргумент имеет странное поведение, заключающееся в цитировании, даже из командной строки.

Другие вопросы по тегам