1 (изменено: Rumata, 2021-09-22 14:01:45)

Тема: 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. Вначале файл скрипта прочитывается, чтобы найти все всех метки блоков данных. Затем файл прочитывается снова, чтобы найти и отобразить текст заданного блока.

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