1

Тема: CMD/BAT: Пакетные макросы с параметрами

С некоторого времени стал почитывать ресурс dostips.com. Наткнулся на интересное решение, выведенное в название темы.

Существует два способа использования повторяющего кода
1. Подпрограмма
2. Макрос

Макрос быстрее так как отсутствуют накладные расходы на (чаще всего) передачу параметров и (всегда) вызов подпрограммы. Тем не менее, использовать макросы в компилируемых языках надо с осторожностью, так как это приводит к увеличению размера программы. Однако это несущественно в языках интерпретируемых.

По этой ссылке Batch "macros" with arguments - Major Update приведена наиболее завершенная реализация макросов. Вообще, интересно почитать все обсуждение (5 страниц). Там много текста и очень много кода, однако если удалить все неплохие "красивости" и оставить только самое главное получится следующее.

Макрос в пакетном сценарии это переменная коружения:


set "my_macro=echo Hello, world!"

%my_macro%
%my_macro%

И так, один макрос - множество вызовов. Но более сложные и полезные макросы писать сложнее.

Для начала рассмотрим подпрограмму CMD/BAT: Нахождение длины строки. Здесь все относительно просто: есть метка, куда осушествляется вызов, запоминается текущее положение, исполняется код, затем возврат на текущее положение


@echo off
setlocal

set "STRING=hello, world! from function"
call :strlen STRING STRLEN
set STR

endlocal
goto :EOF


:: call :strlen STRVAR [STRLEN]
:strlen
    setlocal enabledelayedexpansion
    set "str=A!%~1!"
    set "len=0"
    for /l %%A in (12,-1,0) do (
        set /a "len|=1<<%%A"
        for %%B in (!len!) do if "!str:~%%B,1!"=="" set /a "len&=~1<<%%A"
    )
    for %%v in (!len!) do endlocal & if "%~2" neq "" (set %~2=%%v) else (echo:%%v)
goto :EOF

Чтобы перевести этот код в макрос требуется существенно дописать. Обратите внимание, как производится конкатенация строк кода, так чтобы сохранить прежнее форматирование и правильность кода:


@echo off
setlocal

:: макрос, который позволяет передавать параметры вызываемым макросам
set macro.call=for /f "tokens=1-26" %%a in

:: макрос вычисления длины строки
set macro.strlen=do (^
    setlocal enableDelayedExpansion^
    ^& set "str=A!%%~a!"^
    ^& set "len=0"^
    ^& ( for /l %%A in (12,-1,0) do^
        set /a "len|=1<<%%A"^
        ^& for %%B in (!len!) do if "!str:~%%B,1!"=="" set /a "len&=~1<<%%A"^
    )^
    ^&(for %%v in (!len!) do endlocal^&if "%%~b" neq "" (set %%~b=%%v) else (echo:%%v)^
)

set "STRING=hello, world! from macro"
%macro.call% ( "STRING STRLEN" ) %macro.strlen%
set STR

endlocal
goto :EOF

Коллеги в том обсуждении пошли чуть дальше и добавили немного синтаксическогосахара. Кажется так горяздо лучше - достаточно позаботиться о том, чтобы в конце каждой строки присутствовал макрос %\n%. Но необходимо не забывать об экранировании спец.символов: &, |, <, >.


@echo off
setlocal

:: Определяет переменную ВОЗВРАТ КАРЕТКИ (используется как !CR!)
for /f %%a in ('copy /Z "%~dpf0" nul') do set "CR=%%a"

:: Определяет переменную ПЕРЕВОД СТРОКИ (используется как !LF!)
set LF=^


:: Две пустые строки сверху важны - не удалять!

:: ПЕРЕВОД СТРОКИ, который может использоваться как %xLF%
set ^"xLF=^^^%LF%%LF%^%LF%%LF%"

:: Определяет новую строку с продолжением строки
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"

:: макрос, который позволяет передавать параметры вызываемым макросам
set macro.call=for /f "tokens=1-26" %%a in

:: макрос вычисления длины строки
set macro.strlen=do (%\n%
    setlocal enabledelayedexpansion%\n%
    set "str=A!%%~a!"%\n%
    set "len=0"%\n%
    for /l %%A in (12,-1,0) do (%\n%
        set /a "len|=1<<%%A"%\n%
        for %%B in (!len!) do if "!str:~%%B,1!"=="" set /a "len&=~1<<%%A"%\n%
    )%\n%
    for %%v in (!len!) do endlocal ^& if "%%~b" neq "" (set %%~b=%%v) else (echo:%%v)%\n%
)

set "STRING=hello, world! from macro"
%macro.call% ( "STRING STRLEN" ) %macro.strlen%
set STR

endlocal
goto :EOF

Личное мнение - красиво, но сложно для использования в большинстве сценариев. Но может быть весьма полезна там, где требуется скорость. Но стоит ли для этого использовать интерпретатор?

Хотя коллега с того форума написал на чистом dos shell игру Змейка, используя описанную здесь технологию.

( 2 * b ) || ! ( 2 * b )

2

Re: CMD/BAT: Пакетные макросы с параметрами

А в макросах можно использовать а) независимые переменные окружения и б) возврат значений, как это можно сделать в процедурах?

3

Re: CMD/BAT: Пакетные макросы с параметрами

Английский форум не читал — много непонятных букв.
Как я сам понимаю, для того, чтобы макросу можно было передавать параметр, макрос на самом деле надо разделить на две макроподстановки («голову» и «хвост») и уже фактическое значение параметра записывать между ними, но это удобно, если параметр один. Если несколько, то, чтобы не разбивать макрос на n+1 составляющую, надо уже ухищряться, напрмер, использовать цикл (FOR /F "TOKENS=…"?) для разбора списка параметров. — Да, присмотрелся к третьему примеру — так оно и есть, причём FOR/F… вынесено в однотипную для всех макросов «голову», «хвост» — тело цикла — основная рабочая часть макроса, переменные цикла — формальные параметры… Но так можно передавать только строки, не содержащие пробелов, и кавычки здесь не помогут. Хотя параметром может быть и имя переменной: в самом имени пробелов нет, а в её значении — есть… — Опять я, кажется, угадал.

alexii пишет:

А в макросах можно использовать а) независимые переменные окружения

Вероятно через CALL, если я понял вопрос:

setlocal enabledelayedexpansion

set macro=call echo Hello, %%username%%^^^^^^^^^^!
%macro%
set username=misha
%macro%
echo:
set "macro=call echo Hello, %%username%%^^^^^!"
%macro%
set username=vasya
%macro%
echo:
set macro=echo Hello, ^^^!username^^^!^^^^^^^^^^!
%macro%
set username=petya
%macro%
echo:
set "macro=echo Hello, ^!username^!^^^^^!"
%macro%
set username=sasha
%macro%
alexii пишет:

б) возврат значений, как это можно сделать в процедурах?

Ну, вроде бы, в третьем и четвёртом примерах задуман возврат значения через переменную %STRLEN%, но в третьем он что-то он не работает, а четвёртый возвращает:

STRING=hello, world! from macro
STRLEN=24

4

Re: CMD/BAT: Пакетные макросы с параметрами

alexii пишет:

А в макросах можно использовать а) независимые переменные окружения и б) возврат значений, как это можно сделать в процедурах?

Надо бы уточнить, что значит "независимые переменные". Верно ли я понимаю, что Вы подразумеваете переменные, имена которых не пересекаются с именами основной программы? Другими словами, нет ограничений на именование переменных внутри макросов?

Посмотрите примеры в моем сообщении. Там есть ответы на Ваши вопросы.

В случае с независимыми переменными (так как я понял Вас) работает пара команд setlocal/endlocal.
Макросы могут возвращать значения.

Вообще-то если Вы будете следовать неким общим требованиям, то макросы будут мало чем отличаться от подпрограмм. Например,
1. любой повторно используемый код должен использовать команды setlocal/endlocal для локализации переменных окружения
2. подпрограммы и макросы могут принимать в качестве необязательного параметра имя внешней (по отношению к вызываемому объекту) переменной для передачи вызывающей программе результата выполнения.

( 2 * b ) || ! ( 2 * b )

5

Re: CMD/BAT: Пакетные макросы с параметрами

Rumata пишет:

Надо бы уточнить, что значит "независимые переменные".

Вот это имелось в виду: CMD/BAT: возврат значений из процедур через параметры. Независимые внутренние переменные и возможность возвращать значения из процедуры (через имя процедуры или имена переданных параметрами переменных окружения).

6 (изменено: Rumata, 2013-12-31 18:07:45)

Re: CMD/BAT: Пакетные макросы с параметрами

Ах, да!

3. Параметры подпрограммам передаются через позиционные переменные вида %1, %2, ... %9. Параметры в макросы передаются с помощью позоционных переменных вида %%a, %%b, ... %%z.
4. Вызов подпрограмм и передача параметров осуществляются с помощью команжы call :метка аргументы. Вызов макроса осущетсвляется с помощью команды %имя_макроса%. Вызов макроса и передача параметров осуществляется с помощью команды %macro.call% ( "параметры: ) %имя_макроса%.

( 2 * b ) || ! ( 2 * b )