Тема: CMD/BAT: Код и данные в одном файле
Дальше будет много текста. Желающие могут дочитать до конца, потестировать примеры и поучаствовать в критике (-:
Вступление
В Perl есть такая удобная вещь как виртуальный файл данных, обозначаемый токенами __DATA__ и __END__. Эти токены используются в качестве маркеров логического окончания скрипта и начала блока данных. А весь последующий текст игнорируется интерпретатором и является своеобразным хранилищем данных, который можно прочитать с помощью специального файлового дескриптора DATA. Чуть подробнее об этом написано здесь https://perldoc.perl.org/perldata#Special-Literals.
Также в архивах MetaCPAN есть модуль Inline::Files, который расширяет стандартные возможности языка и позволяет хранить множественные виртуальные файлы.
Я решил попробовать реализовать возможности этого модуля. И кажется получилось. Несколько версий. Каждая имеет реализует (или привносит) свои цели и имеет несколько отличащийся синтаксис объявления блоков данных.
0. Начальная версия
Недавно я переписал один свой скрипт и воспроизвел похожий подход в пакетном скрипте. Реализацию можно посмотреть в этом файле batch/git-md-html.bat.
Здесь фрагмент реализации, дополненный моими коментариями:
...
rem прочитать скрипт
rem пропустить все строки до строки __DATA__
rem вывести все строки ниже строки __DATA__
set "DATA_FOUND="
for /f "usebackq tokens=* delims=" %%s in ( "%~f0" ) do (
if defined DATA_FOUND echo:%%s
if "%%~s" == "__DATA__" set DATA_FOUND=1
)
...
__DATA__
начиная с этой строки будет использован весь текст до конца файла
...
1. Простота превыше безопасности
Здесь основной упор на простоту использования. При этом скрипт не защищен: то есть явно перед блоком данных необходимо указать команды goto :EOF или exit /b ..., чтобы предохранить скрипт от продолжения интерпретации. Блок данных начинается со строки
<начало строки> __<цифры, латинские буквы или подчеркивание>__ <конец строки>
и продолжается до конца файла или следующей строки, соответствующей указанному синтаксису.
Короткий пример:
call :extract_data DATA
...
__DATA__
...
Расширенный пример:
@echo off
setlocal enabledelayedexpansion
call :extract_data WHO_AM_I :parse
call :extract_data DATA
goto :EOF
:parse
echo:%~1
goto :EOF
:extract_data
echo:====
if defined DATA_NAMES goto :extract_data_continue
setlocal enabledelayedexpansion
set "DATA_NAMES="
for /f "tokens=* delims=" %%a in ( '
findstr /r "^__[0-9A-Za-z_][0-9A-Za-z_]*__$" "%~f0"
' ) do set "DATA_NAMES=!DATA_NAMES! %%a"
endlocal & set "DATA_NAMES=%DATA_NAMES%"
:extract_data_continue
set "DATA_THIS="
for /f "tokens=1,* delims=:" %%a in ( '
findstr /n /r "^" "%~f0"
' ) do if "%%b" == "__%~1__" (
set "DATA_THIS=%~1"
) else if defined DATA_THIS (
for %%c in ( %DATA_NAMES% ) do if "%%b" == "%%c" goto :EOF
setlocal disabledelayedexpansion
if "%~2" == "" (
echo:%%b
) else (
call %~2 "%%b"
)
endlocal
)
goto :EOF
__DATA__
there is data block
with 2 percents %%,
2 carets: ^ and ^
and 1 exclamation mark!
You can see all of them.
__WHO_AM_I__
Hello! I am "%USERNAME%"
My homedir: "%USERPROFILE%"
2. Безопасность превыше простоты
Это усовершенствованная версия. Здесь сделан упор на безопасность кода: то есть блоки данных защищены и никогда не будут интерпретироваться и исполняться. Но здесь синтаксис меток данных заметно усложнился и требует большей строгости (в силу бедных возможностей самого интерпретатора и команды findstr). Блок данных начинается со строки:
<начало строки> goto <пробел> :EOF <пробел> & <пробел> :<цифры, латинские буквы или подчеркивание> <конец строки>
и продолжается до конца файла или следующей строки, соответствующей указанному синтаксису.
Короткий пример:
call :extract_data DATA
...
goto :EOF & :DATA
...
Расширенный пример:
@echo off
setlocal enabledelayedexpansion
call :extract_data WHO_AM_I :parse
call :extract_data DATA
goto :EOF
:parse
echo:%~1
goto :EOF
:extract_data
echo:====
if defined DATA_NAMES goto :extract_data_continue
setlocal enabledelayedexpansion
set "DATA_NAMES="
for /f "tokens=3 delims=:" %%a in ( '
findstr /r /c:"^goto :EOF & :[0-9A-Za-z_][0-9A-Za-z_]*$" "%~f0"
' ) do set "DATA_NAMES=!DATA_NAMES! %%a"
endlocal & set "DATA_NAMES=%DATA_NAMES%"
:extract_data_continue
set "DATA_THIS="
for /f "tokens=1,* delims=:" %%a in ( '
findstr /n /r "^" "%~f0"
' ) do if "%%b" == "goto :EOF & :%~1" (
set "DATA_THIS=%~1"
) else if defined DATA_THIS (
for %%c in ( %DATA_NAMES% ) do if "%%b" == "goto :EOF & :%%c" goto :EOF
setlocal disabledelayedexpansion
if "%~2" == "" (
echo:%%b
) else (
call %~2 "%%b"
)
endlocal
)
goto :EOF
goto :EOF & :DATA
there is data block
with 2 percents %%,
2 carets: ^ and ^
and 1 exclamation mark!
You can see all of them.
goto :EOF & :WHO_AM_I
Hello! I am "%USERNAME%"
My homedir: "%USERPROFILE%"
Общее для обеих версий
Подпрограмма поиска и отображения блоков данных реализована в виде подпрограммы :extract_data, которая вызывается с поараметром - именем блока данных. Также вторым аргументом, можно указать имя команды, подпрограммы скрипта или внешней программы для пользовательской обработки данных. Однако это экспериментальная особенность и обладает рядом существенных ограничений на допустимые символы в тексте.
Файл считывается как минимум 2 раза, а точнее - количество вызовов печати блока данных + 1. Вначале файл скрипта прочитывается, чтобы найти все всех метки блоков данных. Затем файл прочитывается снова, чтобы найти и отобразить текст заданного блока.