Как интерпретатор сценариев Windows (CMD.EXE) анализирует сценарии?

Я столкнулся с ss64.com, который дает хорошую помощь в отношении написания пакетных сценариев, которые будет запущен интерпретатор команд Windows.

Однако я не смог найти хорошее объяснение грамматики пакетных скриптов, как вещи расширяются или не расширяются, и как избежать событий.

Вот примеры вопросов, которые я не смог решить:

  • Как управляется система котировок? Я сделал TinyPerl script
    (foreach $i (@ARGV) { print '*' . $i ; }), скомпилировал его и назвал его следующим образом:
    • my_script.exe "a ""b"" c" → вывод *a "b*c
    • my_script.exe """a b c""" → вывести его *"a*b*c"
  • Как работает внутренняя команда echo? Что расширилось внутри этой команды?
  • Почему мне приходится использовать for [...] %%I в сценариях файлов, но for [...] %I в интерактивных сеансах?
  • Каковы escape-символы и в каком контексте? Как избежать знака процента? Например, как я могу эхо %PROCESSOR_ARCHITECTURE% буквально? Я обнаружил, что echo.exe %""PROCESSOR_ARCHITECTURE% работает, есть ли лучшее решение?
  • Как совпадают пары %? Пример:
    • set b=a, echo %a %b% c%%a a c%
    • set a =b, echo %a %b% c%bb c%
  • Как обеспечить, чтобы переменная передавалась команде как один аргумент, если эта переменная содержит двойные кавычки?
  • Как хранятся переменные при использовании команды set? Например, если я делаю set a=a" b, а затем echo.%a%, я получаю a" b. Однако, если я использую echo.exe из UnxUtils, я получаю a b. Как происходит %a% расширяется по-другому?

Спасибо за ваши огни.

Ответ 1

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

Анализатор Batch Line:

Вот краткий обзор этапов обработки строки кода в командном файле:

Этап 0) Читать строку:

Этап 1) Процент расширения:

Этап 1.5) Удалить <CR>: Удалить все символы возврата каретки (0x0D)

Этап 2) Обработка специальных символов, токенизация и создание кэшированного блока команд: Это сложный процесс, на который влияют такие вещи, как кавычки, специальные символы, разделители токенов и экранирование кареток.

Этап 3) Отобразить проанализированные команды, только если командный блок не начинался с @, и ECHO был включен в начале предыдущего шага.

Этап 4) Расширение переменной FOR %X: Только если команда FOR активна и команды после DO обрабатываются.

Этап 5) Отложенное расширение: Только если включено отложенное расширение

Этап 5.3) Обработка канала: Только если команды находятся на любой стороне канала

Этап 5.5) Выполнить перенаправление:

Этап 6) Обработка CALL/Удвоение каретки: Только если токен команды CALL

Этап 7) Выполнить: Команда выполнена


Вот подробности для каждого этапа:

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

Фаза 0) Читать строку: Читать строку ввода через первый <LF>.

  • При чтении строки, которая будет проанализирована как команда, <Ctrl-Z> (0x1A) читается как <LF> (LineFeed 0x0A)
  • Когда GOTO или CALL читают строки во время сканирования на наличие метки: <Ctrl-Z>, они обрабатываются как сами по себе - они не преобразуются в <LF>

Этап 1) Процент расширения:

  • Двойной %% заменяется одним %
  • Расширение аргументов (%*, %1, %2 и т.д.)
  • Расширение %var%, если var не существует, заменить его ничем
  • Сначала строка усекается <LF>, но не в расширении %var%
  • Для полного объяснения прочитайте первую половину из dbenham Та же тема: Percent Phase

Этап 1.5) Удалить <CR>: Удалить все возврат каретки (0x0D) из строки

Этап 2) Обработка специальных символов, токенизация и создание кэшированного блока команд: Это сложный процесс, на который влияют такие вещи, как кавычки, специальные символы, разделители токенов и экранирование каретки. Далее следует приблизительное описание этого процесса.

На этом этапе важны концепции.

  • Токен - это просто строка символов, которая рассматривается как единое целое.
  • Токены разделены разделителями токенов. Стандартными разделителями токенов являются <space> <tab> ; , = <0x0B> <0x0C> и <0xFF>
    Последовательные разделители токенов рассматриваются как единое целое - между разделителями токенов нет пустых токенов
  • В строке в кавычках нет разделителей токенов. Вся строка в кавычках всегда обрабатывается как часть одного токена. Один токен может состоять из комбинации строк в кавычках и символов без кавычек.

Следующие символы могут иметь особое значение в этой фазе, в зависимости от контекста: ^ ( @ & | < > <LF> <space> <tab> ; , ] = <0x0B> <0x0C> <0xFF>

Посмотрите на каждого персонажа слева направо:

  • Если это каретка (^), следующий символ экранируется, а экранирующая каретка удаляется. Экранированные символы теряют все особое значение (кроме <LF>).
  • Если это цитата ("), переключите флаг цитаты. Если флаг кавычки активен, то только " и <LF> являются специальными. Все остальные символы теряют свое особое значение, пока следующая цитата не отключит флаг кавычки. Невозможно избежать закрывающей цитаты. Все цитируемые символы всегда находятся в одном и том же токене.
  • <LF> всегда выключает флаг кавычки. Другие виды поведения варьируются в зависимости от контекста, но кавычки никогда не меняют поведение <LF>.
    • Побег <LF>
      • <LF> раздет
      • Следующий персонаж сбежал. Если в конце буфера строки, следующая строка считывается и обрабатывается фазами 1 и 1.5 и добавляется к текущей перед экранированием следующего символа. Если следующий символ - <LF>, то он рассматривается как литерал, что означает, что этот процесс не является рекурсивным.
    • Unescaped <LF> не в скобках
      • <LF> удаляется, и разбор текущей строки прекращается.
      • Любые оставшиеся символы в буфере строк просто игнорируются.
    • Unescaped <LF> в блоке FOR IN в скобках
      • <LF> преобразуется в <space>
      • Если в конце буфера строки, следующая строка считывается и добавляется к текущей.
    • Unescaped <LF> в заключенном в скобки командном блоке
      • <LF> преобразуется в <LF><space>, а <space> обрабатывается как часть следующей строки командного блока.
      • Если в конце строки находится буфер, то следующая строка читается и добавляется в пробел.
  • Если это один из специальных символов & | < или >, разделите строку в этой точке для обработки каналов, объединения команд и перенаправления.
    • В случае канала (|) каждая сторона представляет собой отдельную команду (или блок команд), которая получает специальную обработку на этапе 5.3
    • В случае конкатенации команд &, && или || каждая сторона конкатенации рассматривается как отдельная команда.
    • В случае перенаправления <, <<, > или >> предложение перенаправления анализируется, временно удаляется, а затем добавляется в конец текущей команды. Предложение перенаправления состоит из необязательной цифры дескриптора файла, оператора перенаправления и маркера назначения перенаправления.
      • Если токен, который предшествует оператору перенаправления, представляет собой одну цифру, то эта цифра указывает дескриптор файла для перенаправления. Если маркер дескриптора не найден, по умолчанию перенаправление вывода равно 1 (стандартный вывод), а перенаправление ввода по умолчанию равно 0 (стандартный ввод).
  • Если самый первый токен для этой команды (до перемещения перенаправления до конца) начинается с @, то @ имеет особое значение. (@ не является особенным в любом другом контексте)
    • Специальный @ удален.
    • Если ECHO включен, то эта команда вместе со всеми последующими каскадными командами в этой строке исключаются из эхо-сигнала фазы 3. Если @ находится перед открытием (, то весь блок, заключенный в скобки, исключается из эхо-сигнала фазы 3.
  • Круглая скобка процесса (содержит составные операторы в несколько строк):
    • Если анализатор не ищет маркер команды, то ( не является специальным.
    • Если анализатор ищет маркер команды и находит (, тогда запустите новый составной оператор и увеличьте счетчик скобок
    • Если счетчик скобок равен> 0, то ) завершает составной оператор и уменьшает счетчик скобок.
    • Если достигнут конец строки и счетчик скобок равен> 0, то следующая строка будет добавлена к составному оператору (начинается снова с фазы 0)
    • Если счетчик скобок равен 0 и синтаксический анализатор ищет команду, то ) функционирует аналогично оператору REM, если за ним сразу следует разделитель токена, специальный символ, символ новой строки или конец файла
      • Все специальные символы теряют свое значение, кроме ^ (возможна конкатенация строк)
      • По достижении конца логической строки вся "команда" отбрасывается.
  • Каждая команда разбирается на серию токенов. Первый токен всегда обрабатывается как командный токен (после того как специальный @ был удален и перенаправление перенесено в конец).
    • Ведущие разделители токенов перед командным токеном удаляются
    • При разборе токена команды, ( действует как разделитель токена команды, в дополнение к стандартным разделителям токена
    • Обработка последующих токенов зависит от команды.
  • Большинство команд просто объединяют все аргументы после токена команды в один токен аргумента. Все разделители токенов аргументов сохраняются. Параметры аргумента обычно не анализируются до фазы 7.
  • Три команды получают специальную обработку - IF, FOR и REM
    • Если IF разделен на две или три отдельные части, которые обрабатываются независимо. Синтаксическая ошибка в конструкции IF приведет к фатальной синтаксической ошибке.
      • Операция сравнения - это действительная команда, которая проходит всю фазу до фазы 7
        • Все опции IF полностью проанализированы на этапе 2.
        • Последовательные разделители маркеров сворачиваются в одно пространство.
        • В зависимости от оператора сравнения будут определены один или два токена значения.
      • Истинный командный блок представляет собой набор команд после условия и анализируется, как и любой другой командный блок. Если необходимо использовать ELSE, тогда блок True должен быть заключен в скобки.
      • Необязательный блок False command - это набор команд после ELSE. Опять же, этот командный блок анализируется нормально.
      • Командные блоки True и False не переходят автоматически в последующие фазы. Их последующая обработка контролируется на этапе 7.
    • FOR делится на две после DO. Синтаксическая ошибка в конструкции FOR приведет к фатальной синтаксической ошибке.
      • Часть через DO является фактической итерационной командой FOR, которая проходит всю фазу 7
        • Все опции FOR полностью анализируются на этапе 2.
        • Предложение в скобках IN обрабатывает <LF> как <space>. После разбора предложения IN все токены объединяются в один токен.
        • Последовательные разделители токенов без кавычек/без кавычек сворачиваются в единое пространство по всей команде FOR через DO.
      • Часть после DO является командным блоком, который анализируется нормально. Последующая обработка командного блока DO контролируется итерацией на этапе 7.
    • REM, обнаруженный в фазе 2, обрабатывается совершенно иначе, чем все другие команды.
      • Анализируется только один токен аргумента - анализатор игнорирует символы после первого токена аргумента.
      • Команда REM может появиться в выходных данных фазы 3, но команда никогда не выполняется, и исходный текст аргумента отображается - экранирующие символы не удаляются, кроме...
        • Если есть только один токен аргумента, который заканчивается неэкранированным ^, который заканчивает строку, то токен аргумента отбрасывается, а последующая строка анализируется и добавляется в REM. Это повторяется до тех пор, пока не будет более одного токена или последний символ не будет ^.
  • Если жетон команды начинается с :, и это первый раунд фазы 2 (не перезапуск из-за ВЫЗОВА в фазе 6), тогда
    • Маркер обычно рассматривается как неисполненная метка.
      • Остальная часть строки анализируется, однако ), <, >, & и | больше не имеют специального значения. Весь остаток строки считается частью метки "команда".
      • ^ продолжает быть особенным, это означает, что продолжение строки можно использовать для добавления следующей строки к метке.
      • Неисполненная метка в заключенном в скобки блоке приведет к фатальной синтаксической ошибке, если за ней сразу не последует команда или метка выполнения в следующей строке.
        • ( больше не имеет специального значения для первой команды, которая следует за неисполненной меткой.
      • Команда отменяется после завершения анализа метки. Последующие этапы не имеют места для ярлыка
    • Существует три исключения, которые могут привести к тому, что метка, найденная на этапе 2, будет рассматриваться как исполняемая метка, которая продолжает анализировать на этапе 7.
      • Существует перенаправление, которое предшествует токену метки, и на линии имеется конкатенация | pipes или команды &, && или ||.
      • Существует перенаправление, которое предшествует токену метки, а команда находится внутри блока в скобках.
      • Маркер метки является самой первой командой в строке внутри блока, заключенного в скобки, а строка выше завершилась неисполненной меткой.
    • Следующее происходит, когда исполняемая метка обнаружена на этапе 2
      • Метка, ее аргументы и перенаправление исключены из любого эхо-выхода в фазе 3
      • Все последующие составные команды в строке полностью анализируются и выполняются.
    • Для получения дополнительной информации о метках "Выполненные" и "Неисполненные" см. https://www.dostips.com/forum/viewtopic.php?f=3&t=3803&p=55405#p55405

Этап 3) Отобразите проанализированные команды, только если командный блок не начинался с @, и ECHO был включен в начале предыдущего шага.

Этап 4) Расширение переменной FOR %X: Только если команда FOR активна и команды после DO обрабатываются.

  • На этом этапе фаза 1 пакетной обработки уже преобразует переменную FOR, например, %%X, в %X. Командная строка имеет разные правила процентного расширения для фазы 1. По этой причине командные строки используют %X, но командные файлы используют %%X для переменных FOR.
  • Имена переменных FOR чувствительны к регистру, но ~modifiers не чувствительны к регистру.
  • ~modifiers имеет приоритет над именами переменных. Если символ, следующий за ~, является одновременно модификатором и допустимым именем переменной FOR, и существует последующий символ, который является активным именем переменной FOR, тогда этот символ интерпретируется как модификатор.
  • Имена переменных FOR являются глобальными, но только в контексте предложения DO. Если подпрограмма вызывается из предложения FOR DO, то переменные FOR не раскрываются в подпрограмме CALLed. Но если у подпрограммы есть собственная команда FOR, то все переменные, определенные в настоящее время, FOR доступны для внутренних команд DO.
  • Имена переменных FOR могут быть повторно использованы во вложенных FOR. Внутреннее значение FOR имеет приоритет, но после закрытия INNER FOR внешнее значение FOR восстанавливается.
  • Если ECHO был включен в начале этого этапа, то этап 3) повторяется, чтобы показать проанализированные команды DO после расширения переменных FOR.

---- С этого момента каждая команда, определенная на этапе 2, обрабатывается отдельно.
---- Фазы с 5 по 7 завершены для одной команды, прежде чем перейти к следующей.

Этап 5) Отложенное расширение: Только если отложенное расширение включено, команда не находится в блоке с круглыми скобками по обе стороны канала, и команда не является "голым" пакетом сценарий (имя сценария без скобок, CALL, объединение команд или канал).

  • Каждый токен для команды анализируется для отложенного расширения независимо.
    • Большинство команд анализируют два или более токенов - токен команды, токен аргументов и каждый токен назначения перенаправления.
    • Команда FOR анализирует только токен предложения IN.
    • Команда IF анализирует только значения сравнения - одно или два, в зависимости от оператора сравнения.
  • Для каждого проанализированного токена сначала проверьте, есть ли в нем !. Если нет, то токен не анализируется - важно для символов ^. Если токен содержит !, отсканируйте каждый символ слева направо:
    • Если это каретка (^), следующий символ не имеет особого значения, сама каретка удаляется
    • Если это восклицательный знак, ищите следующий восклицательный знак (каретки больше не наблюдаются), разверните значение переменной.
      • Последовательное открытие ! свернуто в один !
      • Все оставшиеся непарные ! удаляются
    • Расширение vars на этом этапе "безопасно", потому что специальные символы больше не обнаруживаются (даже <CR> или <LF>)
    • Для более полного объяснения, прочитайте 2-ую половину этого от dbenham та же тема - Фаза восклицательного знака

Этап 5.3) Обработка канала: Только если команды находятся на любой стороне канала
Каждая сторона канала обрабатывается независимо и асинхронно.

  • Если команда является внутренней для cmd.exe, или это командный файл, или если это командный блок в скобках, то он выполняется в новом потоке cmd.exe через %comspec% /S /D /c" commandBlock", поэтому командный блок получает фазовый перезапуск, но на этот раз в режиме командной строки.
    • Если блок команд заключен в скобки, то все <LF> с командой до и после преобразуются в <space>&. Другие <LF> лишены.
  • Это конец обработки для конвейерных команд.
  • См. Почему отложенное расширение завершается неудачно, когда он находится внутри конвейерного блока кода?, чтобы узнать больше о разборе и обработке канала

Этап 5.5) Выполнить перенаправление: Любое перенаправление, обнаруженное на этапе 2, теперь выполняется.

Этап 6) Обработка CALL/Удвоение каретки: Только если токен команды - CALL или если текст перед первым встречающимся стандартным разделителем токена - CALL. Если CALL анализируется из большего токена команды, то перед продолжением перед неиспользуемой частью ставится токен аргументов.

  • Сканируйте маркер аргументов на предмет отсутствия кавычек /?. Если найден где-нибудь в пределах токенов, прервите фазу 6 и перейдите к Фазе 7, где будет напечатана ПОМОЩЬ для ВЫЗОВА.
  • Удалите первый CALL, чтобы несколько CALL могли быть сложены
  • Удвойте все каретки
  • Перезапустите фазы 1, 1.5 и 2, но не переходите к фазе 3
    • Любые двойные каретки сокращаются до одной каретки, если они не указаны. Но, к сожалению, котировки остаются в два раза.
    • Фаза 1 немного меняется
      • Ошибки расширения на шаге 1.2 или 1.3 отменяют CALL, но ошибка не является фатальной - пакетная обработка продолжается.
    • Задачи Фазы 2 немного изменены
      • Любое вновь появляющееся перенаправленное без кавычек, неэкранированное, которое не было обнаружено в первом раунде фазы 2, обнаружено, но оно удалено (включая имя файла) без фактического выполнения перенаправления
      • Любая вновь появляющаяся нецитированная, неэкранированная каретка в конце строки удаляется без продолжения строки
      • CALL отменяется без ошибок, если обнаруживается любое из следующего
        • Вновь появившиеся без кавычек, неэкранированные & или |
        • Полученный в результате токен команды начинается без кавычек, без экранирования (
        • Самый первый токен после удаленного CALL начался с @
      • Если результирующая команда является, по-видимому, допустимой IF или FOR, то впоследствии выполнение завершится неудачно с ошибкой, сообщающей, что IF или FOR не распознаются как внутренняя или внешняя команда.
      • Разумеется, CALL не прерывается во втором раунде фазы 2, если результирующий токен команды является меткой, начинающейся с :.
  • Если результирующим токеном команды является CALL, перезапустите этап 6 (повторяется до тех пор, пока CALL больше не будет)
  • Если результирующий токен команды представляет собой пакетный сценарий или метку:, то выполнение CALL полностью обрабатывается оставшейся частью этапа 6.
    • Выдвиньте текущую позицию файла пакетного сценария в стеке вызовов, чтобы выполнение могло возобновиться с правильной позиции после завершения вызова.
    • Установите маркеры аргументов% 0,% 1,% 2,...% N и% * для CALL, используя все полученные токены
    • Если маркер команды является меткой, начинающейся с :, то
      • Перезапустите этап 5. Это может повлиять на то, что: CALLED. Но поскольку токены% 0 и т.д. Уже настроены, они не изменят аргументы, передаваемые подпрограмме CALLed.
      • Выполните метку GOTO, чтобы поместить указатель файла в начало подпрограммы (не обращайте внимания на любые другие токены, которые могут следовать за меткой:). См. на этапе 7 правила о том, как работает GOTO.
    • В противном случае передать управление указанному пакетному сценарию.
    • Выполнение метки или сценария CALLed: продолжается до тех пор, пока не будет достигнут EXIT/B или конец файла, после чего стек CALL будет извлечен, и выполнение возобновится с позиции сохраненного файла.
      Этап 7 не выполняется для сценариев CALLed или: меток.
  • В противном случае результат этапа 6 переходит в этап 7 для выполнения.

Этап 7) Выполнить: Команда выполнена

  • 7.1 - Выполнить внутреннюю команду - Если токен команды указан в кавычках, пропустите этот шаг. В противном случае попытайтесь разобрать внутреннюю команду и выполнить.
    • Следующие тесты сделаны, чтобы определить, представляет ли токен команды без кавычек внутреннюю команду:
      • Если маркер команды точно соответствует внутренней команде, выполните ее.
      • Иначе сломайте токен команды до первого появления + / [ ] <space> <tab> , ; или =
        Если предыдущий текст является внутренней командой, то запомните эту команду
        • Если в режиме командной строки или команда из блока в скобках, командного блока ЕСЛИ истина или ложь, командного блока FOR DO или связана с объединением команд, выполните внутреннюю команду
        • В противном случае (должна быть автономной командой в пакетном режиме) сканировать текущую папку и PATH на наличие файла .COM,.EXE,.BAT или .CMD, базовое имя которого соответствует исходному токену команды.
          • Если первый соответствующий файл -.BAT или .CMD, перейдите к 7.3.exec и выполните этот скрипт
          • .В противном случае (совпадение не найдено или первое совпадение -.EXE или .COM) выполните запомненную внутреннюю команду
      • Иначе сломайте токен команды до первого появления . \ или :
        Если предыдущий текст не является внутренней командой, перейдите к 7.2
        В противном случае предыдущий текст может быть внутренней командой. Запомните эту команду.
      • Разбейте маркер команды до первого появления + / [ ] <space> <tab> , ; или =
        Если предыдущий текст является путем к существующему файлу, перейдите к 7.2
        В противном случае выполните запомненную внутреннюю команду.
    • Если внутренняя команда анализируется из большого токена команды, то неиспользованная часть токена команды включается в список аргументов
    • То, что маркер команды анализируется как внутренняя команда, не означает, что она будет выполнена успешно. Каждая внутренняя команда имеет свои собственные правила анализа параметров и параметров и допустимого синтаксиса.
    • Все внутренние команды выводят справку вместо выполнения своих функций, если обнаружен /?. Большинство распознают /?, если оно появляется где-либо в аргументах. Но некоторые команды, такие как ECHO и SET, выводят справку только в том случае, если маркер первого аргумента начинается с /?.
    • У SET есть интересная семантика:
      • Если команда SET содержит кавычку до того, как имя переменной и расширения будут включены
        set "name=content" ignored -> value = content
        затем в качестве содержимого используется текст между первым знаком равенства и последней цитатой (исключая первое равенство и последнюю цитату). Текст после последней цитаты игнорируется. Если после знака равенства нет кавычек, то остальная часть строки используется в качестве содержимого.
      • Если команда SET не имеет кавычки перед именем
        set name="content" not ignored -> value = "content" not ignored
        затем весь остаток строки после равенства используется в качестве содержимого, включая любые и все кавычки, которые могут присутствовать.
    • Сравнение IF вычисляется, и в зависимости от того, является ли условие истинным или ложным, обрабатывается соответствующий уже проанализированный зависимый блок команд, начиная с фазы 5.
    • Предложение IN команды FOR повторяется соответствующим образом.
      • Если это FOR/F, который повторяет вывод командного блока, то:
        • Предложение IN выполняется в новом процессе cmd.exe через CMD/C.
        • Блок команд должен пройти весь процесс синтаксического анализа во второй раз, но на этот раз в контексте командной строки
        • ECHO запустится ВКЛ, а отложенное расширение обычно будет отключено (зависит от настроек реестра)
        • Все изменения среды, сделанные командным блоком предложения IN, будут потеряны после завершения дочернего процесса cmd.exe
      • Для каждой итерации:
        • Определены значения переменных FOR
        • Уже обработанный блок команд DO обрабатывается, начиная с фазы 4.
    • GOTO использует следующую логику для поиска метки:
      • Метка анализируется с токена первого аргумента
      • Сценарий сканируется для следующего появления метки
        • Сканирование начинается с текущей позиции файла
        • Если достигнут конец файла, то сканирование возвращается к началу файла и продолжается до исходной начальной точки.
      • Сканирование останавливается при первом появлении найденной метки, а указатель файла устанавливается на строку, следующую непосредственно за меткой. Выполнение сценария возобновляется с этого момента. Обратите внимание, что успешное истинное GOTO немедленно прервет любой проанализированный блок кода, включая циклы FOR.
      • Если метка не найдена или токен метки отсутствует, то GOTO завершается ошибкой, печатается сообщение об ошибке и извлекается стек вызовов. Это эффективно работает как EXIT/B, за исключением того, что все уже проанализированные команды в текущем командном блоке, которые следуют за GOTO, все еще выполняются, но в контексте CALLer (контекст, который существует после EXIT/B)
      • См. https://www.dostips.com/forum/viewtopic.php?f=3&t=3803 для более точного описания правил, используемых для анализа меток.
    • RENAME и COPY принимают подстановочные знаки для исходного и целевого путей. Но Microsoft делает ужасную работу, документируя, как работают шаблоны, особенно для целевого пути. Полезный набор правил подстановочных знаков можно найти на . Как команда Windows RENAME интерпретирует подстановочные знаки?
  • 7.2 - Выполнить изменение громкости - В противном случае, если токен команды не начинается с кавычки, имеет длину ровно два символа, а второй символ - двоеточие, измените громкость
    • Все токены аргументов игнорируются
    • Если громкость, указанную первым символом, не может быть найдена, прервать с ошибкой
    • Токен команды :: всегда будет приводить к ошибке, если SUBST не используется для определения тома для ::
      Если SUBST используется для определения громкости для ::, то громкость будет изменена, она не будет рассматриваться как метка.
  • 7.3 - Выполнить внешнюю команду - Иначе попытаться обработать команду как внешнюю команду.
    • Если в режиме командной строки команда не заключена в кавычки и не начинается со спецификации тома, разбейте токен команды при первом появлении <space> , ; или = и добавьте остаток к аргументу маркер (ы).
    • Если вторым символом токена команды является двоеточие, проверьте, найден ли объем, заданный первым символом.
      Если том не найден, прервитесь с ошибкой.
    • Если в пакетном режиме токен команды начинается с :, то перейдите к 7.4
      Обратите внимание, что если маркер метки начинается с ::, то он не будет достигнут, поскольку предыдущий шаг будет прерван с ошибкой, если SUBST не используется для определения тома для ::.
    • Определите внешнюю команду для выполнения.
      • Это сложный процесс, который может включать текущий том, текущий каталог, переменную PATH, переменную PATHEXT и/или ассоциации файлов.
      • Если действительная внешняя команда не может быть идентифицирована, прервитесь с ошибкой.
    • Если в режиме командной строки токен команды начинается с :, то перейдите к 7.4
      Обратите внимание, что это редко достигается, потому что предыдущий шаг будет прерван с ошибкой, если токен команды не начинается с ::, а SUBST используется для определения тома для ::, а весь токен команды является допустимым путем к внешняя команда.
    • 7.3.exec - выполнить внешнюю команду.
  • 7.4 - Игнорировать метку - Игнорировать команду и все ее аргументы, если маркер команды начинается с :.
    Правила в 7.2 и 7.3 могут помешать этикетке достичь этой точки.

Анализатор командной строки:

Работает как BatchLine-Parser, кроме:

Этап 1) Процент расширения:

  • Нет %*, %1 и т.д. Расширение аргумента
  • Если var не определено, то %var% остается без изменений.
  • Никакой специальной обработки %%. Если var = content, то %%var%% расширяется до %content%.

Этап 3) Отобразить проанализированные команды

  • Это не выполняется после фазы 2. Это выполняется только после фазы 4 для блока команд FOR DO.

Этап 5) Отсроченное расширение: только если включено DelayedExpansion

  • Если var не определено, то !var! остается без изменений.

Этап 7) Выполнить команду

  • Попытки CALL или GOTO a: label приводят к ошибке.
  • Как уже задокументировано на этапе 7, выполненная метка может привести к ошибке при различных сценариях.
    • Пакетно выполненные метки могут вызвать ошибку, только если они начинаются с ::
    • Выполненные в командной строке метки почти всегда приводят к ошибке

Разбор целочисленных значений

Существует множество различных контекстов, в которых cmd.exe анализирует целочисленные значения из строк, а правила противоречивы:

  • SET /A
  • IF
  • %var:~n,m% (расширение подстроки переменной)
  • FOR /F "TOKENS=n"
  • FOR /F "SKIP=n"
  • FOR /L %%A in (n1 n2 n3)
  • EXIT [/B] n

Подробности этих правил можно найти в Правилах того, как CMD.EXE анализирует числа


.Для тех, кто хочет улучшить правила синтаксического анализа cmd.exe, на форуме DosTips есть тема обсуждения, где можно сообщать о проблемах и вносить предложения.

Надеюсь, это поможет
Ян Эрик (Джеб) - оригинальный автор и первооткрыватель фаз
Dave Benham (dbenham) - много дополнительного контента и редактирования

Ответ 2

При вызове команды из окна командной строки токенизация аргументов командной строки выполняется не с помощью cmd.exe (a.k.a. "shell" ). Чаще всего токенизация выполняется с помощью среды выполнения только что сформированных процессов C/С++, но это не обязательно так, например, если новый процесс не был написан на C/С++ или если новый процесс предпочитает игнорировать argv и обработать необработанную командную строку для себя (например, с помощью GetCommandLine()). На уровне ОС Windows пропускает командные строки, которые не были пронумерованы как одна строка для новых процессов. Это противоречит большинству * nix-оболочек, где оболочка токенизирует аргументы согласованным, предсказуемым образом, прежде чем передавать их во вновь образованный процесс. Все это означает, что вы можете испытывать дивергентное поведение токенизации аргументов в разных программах в Windows, поскольку отдельные программы часто принимают аргументированную токенизацию в свои руки.

Если это звучит как анархия, это своего рода. Однако, поскольку большое количество программ Windows использует среду выполнения Microsoft C/С++ argv, обычно полезно понять как токенизация MSVCRT аргументы. Вот выдержка:

  • Аргументы разделяются пробелом, который является либо пространством, либо вкладкой.
  • Строка, окруженная двойными кавычками, интерпретируется как один аргумент, независимо от пробела, содержащегося внутри. Строка с кавычками может быть встроена в аргумент. Обратите внимание, что каретка (^) не распознается как escape-символ или разделитель.
  • Двойной кавычек, которому предшествует обратная косая черта, \ ", интерпретируется как буквальная двойная кавычка (" ).
  • Обратные косые черты интерпретируются буквально, если только они не предшествуют двойной кавычки.
  • Если четное число обратных косых черт сопровождается знаком двойной кавычки, то одна обратная косая черта() помещается в массив argv для каждой пары обратных косых черт (\), а знак двойной кавычки (") интерпретируется как строка разделитель.
  • Если за нечетным числом обратных косых черт следует двойная кавычка, тогда одна обратная косая черта() помещается в массив argv для каждой пары обратных косых черт (\), а знак двойной кавычки интерпретируется как escape-последовательность оставшимися обратная косая черта, в результате чего в argv помещается буквальная двойная кавычка (").

"пакетный язык" Microsoft (.bat) не является исключением для этой анархической среды, и он разработал свои собственные уникальные правила для токенизации и экранирования. Также похоже, что командлет командной строки cmd.exe выполняет некоторую предварительную обработку аргумента командной строки (в основном для замены переменных и экранирования), прежде чем передать аргумент в новый процесс. Вы можете больше узнать о низкоуровневых деталях языкового пакета и cmd-экранизации в превосходных ответах jeb и dbenham на этой странице.


Позвольте построить простую утилиту командной строки в C и посмотреть, что она говорит о ваших тестовых случаях:

int main(int argc, char* argv[]) {
    int i;
    for (i = 0; i < argc; i++) {
        printf("argv[%d][%s]\n", i, argv[i]);
    }
    return 0;
}

(Примечания: argv [0] всегда является именем исполняемого файла и для краткости ниже приводится ниже. Протестировано в Windows XP с пакетом обновления 3. Скомпилировано с помощью Visual Studio 2005.)

> test.exe "a ""b"" c"
argv[1][a "b" c]

> test.exe """a b c"""
argv[1]["a b c"]

> test.exe "a"" b c
argv[1][a" b c]

И несколько моих собственных тестов:

> test.exe a "b" c
argv[1][a]
argv[2][b]
argv[3][c]

> test.exe a "b c" "d e
argv[1][a]
argv[2][b c]
argv[3][d e]

> test.exe a \"b\" c
argv[1][a]
argv[2]["b"]
argv[3][c]

Ответ 3

Правила расширения процента

Вот расширенное объяснение Фазы 1 в jeb answer (Действительно как для пакетного режима, так и для режима командной строки).

Этап 1) Процентное расширение Начиная слева, отсканируйте каждый символ на предмет % или <LF>. Если найдено, то

  • 1,05 (усеченная строка в <LF>)
    • Если персонаж <LF>, то
      • Удалить (игнорировать) оставшуюся часть строки с <LF> и далее
      • Перейти к этапу 1.5 (полоса <CR>)
    • В противном случае символ должен быть %, поэтому перейдите к 1.1
  • 1.1 (escape %) пропускается, если режим командной строки
    • Если в пакетном режиме следует другой %, то
      Замените %% на один % и продолжите сканирование
  • 1.2 (развернуть аргумент) пропускается, если в режиме командной строки
    • Иначе если пакетный режим то
      • Если после этого * и расширения команд включены, то
        Замените %* текстом всех аргументов командной строки (Замените ничем, если аргументов нет) и продолжите сканирование.
      • Иначе, если за ним следует <digit>, то
        Замените %<digit> значением аргумента (замените ничем, если не определено) и продолжите сканирование.
      • Иначе, если за ним следует ~ и расширения команд включены
        • Если за ним следует необязательный допустимый список модификаторов аргумента, за которым следует обязательный <digit>, тогда
          Замените %~[modifiers]<digit> измененным значением аргумента (замените его ничем, если он не определен или если указан $ PATH: модификатор не определен) и продолжите сканирование.
          Примечание: модификаторы чувствительны к регистру и могут появляться несколько раз в любом порядке, кроме $ PATH: модификатор может появляться только один раз и должен быть последним модификатором перед <digit>
        • В противном случае неверный синтаксис измененного аргумента вызывает фатальную ошибку: все проанализированные команды прерываются, и пакетная обработка прерывается, если в пакетном режиме!
  • 1.3 (развернуть переменную)
    • Иначе, если расширения команд отключены,
      Посмотрите на следующую строку символов, разбивающуюся перед % или концом буфера, и назовите их VAR (может быть пустой список)
      • Если следующий символ - %, тогда
        • Если VAR определен, то
          Замените %VAR% значением VAR и продолжите сканирование
        • Иначе, если в пакетном режиме, то
          Удалите %VAR% и продолжите сканирование
        • Остальное до 1.4
      • Остальное до 1.4
    • Иначе, если расширения команд включены, тогда
      Посмотрите на следующую строку символов, разбивающуюся перед % : или концом буфера, и назовите их VAR (может быть пустой список). Если VAR прерывается до : и последующим символом является %, тогда включите : в качестве последнего символа в VAR и разбейте перед %.
      • Если следующий символ - %, тогда
        • Если VAR определен, то
          Замените %VAR% значением VAR и продолжите сканирование
        • Иначе, если в пакетном режиме, то
          Удалите %VAR% и продолжайте сканирование
        • Остальное до 1.4
      • Иначе, если следующий символ :, то
        • Если VAR не определен, то
          • Если в пакетном режиме, то
            Удалите %VAR: и продолжите сканирование.
          • Остальное до 1.4
        • Иначе, если следующий символ ~, то
          • Если следующая строка символов соответствует шаблону из [integer][,[integer]]%, тогда
            Замените %VAR:~[integer][,[integer]]% подстрокой значения VAR (возможно, результатом будет пустая строка) и продолжите сканирование.
          • Остальное до 1.4
        • Иначе, если за ним следует = или *=, то
          Неверный синтаксис поиска и замены переменных вызывает фатальную ошибку: все проанализированные команды прерываются, а пакетная обработка прерывается, если в пакетном режиме!
        • Иначе, если следующая строка символов соответствует шаблону [*]search=[replace]%, где поиск может включать любой набор символов, кроме =, а замена может включать любой набор символов, кроме %, затем замените
          %VAR:[*]search=[replace]% со значением VAR после выполнения поиска и замены (возможно, приводящего к пустой строке) и продолжения сканирования
        • Остальное до 1.4
  • 1,4 (полоса%)
    • Иначе, если в пакетном режиме, то
      Удалите % и продолжите сканирование, начиная со следующего символа после %
    • В противном случае сохраните % и продолжите сканирование, начиная со следующего символа после %

Вышеприведенное помогает объяснить, почему эта партия

@echo off
setlocal enableDelayedExpansion
set "1var=varA"
set "~f1var=varB"
call :test "arg1"
exit /b  
::
:test "arg1"
echo %%1var%% = %1var%
echo ^^^!1var^^^! = !1var!
echo --------
echo %%~f1var%% = %~f1var%
echo ^^^!~f1var^^^! = !~f1var!
exit /b

Дает эти результаты:

%1var% = "arg1"var
!1var! = varA
--------
%~f1var% = P:\arg1var
!~f1var! = varB

Примечание 1 - Фаза 1 происходит до признания операторов REM. Это очень важно, потому что это означает, что даже замечание может привести к фатальной ошибке, если у него недопустимый синтаксис расширения аргумента или неверный синтаксис поиска и замены переменных!

@echo off
rem %~x This generates a fatal argument expansion error
echo this line is never reached

Примечание 2 - еще одно интересное следствие правил синтаксического анализа%: переменные, содержащие в имени: могут быть определены, но их нельзя развернуть, если не отключены расширения команд. Есть одно исключение - имя переменной, содержащее один двоеточие в конце, может быть расширено, если включены расширения команд. Однако вы не можете выполнять подстроку или операции поиска и замены над именами переменных, заканчивающимися двоеточием. Пакетный файл ниже (любезно предоставлен Jeb) демонстрирует это поведение

@echo off
setlocal
set var=content
set var:=Special
set var::=double colon
set var:~0,2=tricky
set var::~0,2=unfortunate
echo %var%
echo %var:%
echo %var::%
echo %var:~0,2%
echo %var::~0,2%
echo Now with DisableExtensions
setlocal DisableExtensions
echo %var%
echo %var:%
echo %var::%
echo %var:~0,2%
echo %var::~0,2%

Примечание 3 - Интересный результат порядка правил синтаксического анализа, который Джеб изложил в своем посте: при выполнении поиска и замены с отложенным раскрытием специальные символы в терминах поиска и замены должны быть экранированы или заключены в кавычки, Но ситуация с процентным расширением другая - нельзя найти термин поиска (хотя его можно цитировать). Строка процента замены может или не может требовать экранирования или кавычки, в зависимости от ваших намерений.

@echo off
setlocal enableDelayedExpansion
set "var=this & that"
echo %var:&=and%
echo "%var:&=and%"
echo !var:^&=and!
echo "!var:&=and!"

Правила отложенного расширения

Вот расширенное и более точное объяснение этапа 5 в расширенном ответе (действительно как для пакетного режима, так и для режима командной строки)

Этап 5) Отсроченное расширение

Эта фаза пропускается, если выполняется любое из следующих условий:

  • Задержка расширения отключена.
  • Команда находится в заключенном в скобки блоке по обе стороны канала.
  • Токен входящей команды является "пустым" пакетным сценарием, то есть он не связан с CALL, блоком в скобках, какой-либо формой объединения команд (&, && или ||) или каналом |.

Процесс отложенного расширения применяется к токенам независимо. У команды может быть несколько токенов:

  • Командный токен. Для большинства команд само имя команды является токеном. Но у некоторых команд есть специализированные регионы, которые считаются символом для фазы 5.
    • for ... in(TOKEN) do
    • if defined TOKEN
    • if exists TOKEN
    • if errorlevel TOKEN
    • if cmdextversion TOKEN
    • if TOKEN comparison TOKEN, где сравнение является одним из ==, equ, neq, lss, leq, gtr или geq
  • Аргументы токена
  • Маркер перенаправления назначения (по одному на перенаправление)

В токены, не содержащие !, не вносятся изменения.

Для каждого токена, который содержит хотя бы один !, отсканируйте каждый символ слева направо на предмет ^ или ! и, если он найден, то

  • 5.1 (выход каретки) Необходим для литералов ! или ^
    • Если персонаж является каретой ^, тогда
      • Удалить ^
      • Отсканируйте следующий символ и сохраните его как литерал
      • Продолжить сканирование
  • 5.2 (развернуть переменную)
    • Если символ !, то
      • Если расширения команд отключены,
        Посмотрите на следующую строку символов, разбиваясь перед ! или <LF>, и назовите их VAR (может быть пустой список)
        • Если следующий символ - !, тогда
          • Если VAR определен, то
            Замените !VAR! значением VAR и продолжите сканирование
          • Иначе, если в пакетном режиме, то
            Удалить !VAR! и продолжить сканирование
          • Остальное goto 5.2.1
        • Остальное goto 5.2.1
      • Иначе, если расширения команд включены, тогда
        Посмотрите на следующую строку символов, разбитую перед !, : или <LF>, и назовите их VAR (может быть пустой список). Если VAR прерывается до : и последующим символом является !, тогда включите : в качестве последнего символа в VAR и разбейте перед !
        • Если следующий символ - !, тогда
          • Если VAR существует, то
            Замените !VAR! значением VAR и продолжите сканирование
          • Иначе, если в пакетном режиме, то
            Удалите !VAR! и продолжите сканирование
          • Остальное goto 5.2.1
        • Иначе, если следующий символ :, то
          • Если VAR не определен, то
            • Если в пакетном режиме, то
              Удалите !VAR: и продолжите сканирование
            • Остальное goto 5.2.1
          • Иначе, если следующая строка символов соответствует шаблону
            ~[integer][,[integer]]! тогда
            Замените !VAR:~[integer][,[integer]]! подстрокой значения VAR (возможно, результатом будет пустая строка) и продолжите сканирование
          • Иначе, если следующая строка символов совпадает с шаблоном [*]search=[replace]!, где поиск может включать любой набор символов, кроме =, а замена может включать любой набор символов, кроме !, тогда
            Замените !VAR:[*]search=[replace]! на значение VAR после выполнения поиска и замените (возможно, получится пустая строка) и продолжите сканирование
          • Остальное goto 5.2.1
        • Остальное goto 5.2.1
      • 5.2.1
        • В пакетном режиме удалите !
          Остальное сохраните !
        • Продолжите сканирование, начиная со следующего символа после !

Ответ 4

Как указано, командам передается вся строка аргумента на земле μSoft, и им приходится разбирать это в отдельные аргументы для собственного использования. В этом нет никакой взаимосвязи между различными программами, и поэтому для описания этого процесса нет единого набора правил. Вам действительно нужно проверить каждый угловой регистр для любой библиотеки C, которую использует ваша программа.

Что касается файлов системы .bat, вот этот тест:

c> type args.cmd
@echo off
echo cmdcmdline:[%cmdcmdline%]
echo 0:[%0]
echo *:[%*]
set allargs=%*
if not defined allargs goto :eof
setlocal
@rem Wot about a nice for loop?
@rem Then we are in the land of delayedexpansion, !n!, call, etc.
@rem Plays havoc with args like %t%, a"b etc. ugh!
set n=1
:loop
    echo %n%:[%1]
    set /a n+=1
    shift
    set param=%1
    if defined param goto :loop
endlocal

Теперь мы можем запустить несколько тестов. Посмотрите, можете ли вы понять, что именно делают μSoft:

C>args a b c
cmdcmdline:[cmd.exe ]
0:[args]
*:[a b c]
1:[a]
2:[b]
3:[c]

Прекрасно. (Теперь я оставлю неинтересные %cmdcmdline% и %0.)

C>args *.*
*:[*.*]
1:[*.*]

Нет расширения имени файла.

C>args "a b" c
*:["a b" c]
1:["a b"]
2:[c]

Отсутствие цитаты, но кавычки не позволяют разделять аргументы.

c>args ""a b" c
*:[""a b" c]
1:[""a]
2:[b" c]

Последовательные двойные кавычки заставляют их терять любые специальные возможности синтаксического анализа, которые они могли иметь. Пример @Beniot:

C>args "a """ b "" c"""
*:["a """ b "" c"""]
1:["a """]
2:[b]
3:[""]
4:[c"""]

Quiz: Как передать значение любой среды var в виде единственного аргумента (т.е. как %1) в файл bat?

c>set t=a "b c
c>set t
t=a "b c
c>args %t%
1:[a]
2:["b c]
c>args "%t%"
1:["a "b]
2:[c"]
c>Aaaaaargh!

Sane parsing кажется навсегда сломанным.

Для вашего развлечения попробуйте добавить в эти примеры разные символы ^, \, ', & (& c.).

Ответ 5

У вас уже есть отличные ответы, но ответьте на одну часть вашего вопроса:

set a =b, echo %a %b% c% → bb c%

Что происходит, потому что у вас есть пробел перед =, создается переменная под названием %a<space>% поэтому, когда вы echo %a %, который правильно оценивается как b.

Затем оставшаяся часть b% c% вычисляется как обычный текст + переменная undefined % c%, которая должна отображаться как введенная, для меня echo %a %b% c% возвращает bb% c%

Я подозреваю, что возможность включать пробелы в имена переменных является скорее надзором, чем запланированной "особенностью"

Ответ 6

изменить: см. принятый ответ, что следует неправильно и объясняет только, как передать командную строку TinyPerl.


Что касается кавычек, у меня возникает ощущение, что поведение таково:

  • когда найдено ", начало строки начинается
  • когда происходит чередование строк:
    • каждый символ, который не является ", является globbed
    • когда найдено ":
      • если за ним следует "" (таким образом, тройка "), тогда двойная кавычка добавляется к строке
      • если за ним следует " (таким образом, double "), тогда в строку добавляется двойная кавычка, а завершение строки заканчивается
      • Если следующий символ не ", завершение строки завершается
    • когда заканчивается строка, завершение строки завершается.

Короче:

"a """ b "" c""" состоит из двух строк: a " b " и c"

"a"", "a""" и "a"""" - это одна и та же строка, если в конце строки