Как упаковать все мои функции в командный файл в отдельный файл?

Мой вопрос связан с этим вопросом. У меня есть несколько действий, которые необходимо выполнить из командного файла, и я хотел бы смоделировать их как функции и вызов из главной последовательности. Из приведенного выше вопроса ясно, что я могу сделать это с помощью синтаксиса вызова

call:myDosFunc

Мой вопрос заключается в том, можно ли поместить все эти функции в отдельный пакетный файл (functions.bat) и каким-то образом "включить" его в основной пакетный файл и вызвать их? Другой вариант - использовать возможность вызова functions.bat из main.bat с синтаксисом вызова, но я не уверен, смогу ли я вызвать это с определенной функцией вместо выполнения всего командного файла.

Короче говоря, я ищу что-то похожее на мир программирования на C, где мои функции находятся в DLL, а основная программа содержит только высокоуровневую логику и вызывает функции из DLL.

5 ответов

Решение

Вот простой пример того, как это можно сделать.

Сценарий функции вызывается с именем функции в качестве первого аргумента и аргументами функции как arg2, arg3, ...

При условии, что он вызывается правильно, скрипт сдвигает аргументы и выполняет GOTO к исходному arg1. Тогда у функции есть свои аргументы, начинающиеся с нового arg1. Это означает, что вы можете взять уже написанные подпрограммы и добавить их в утилиту, не беспокоясь о корректировке номеров параметров.

Сценарий выдает ошибку, если аргумент функции не указан или аргумент функции не соответствует допустимой метке в скрипте.

@echo off
if "%~1" neq "" (
  2>nul >nul findstr /rc:"^ *:%~1\>" "%~f0" && (
    shift /1
    goto %1
  ) || (
    >&2 echo ERROR: routine %~1 not found
  )
) else >&2 echo ERROR: missing routine
exit /b

:test1
echo executing :test1
echo arg1 = %1
exit /b

:test2
echo executing :test2
echo arg1 = %1
echo arg2 = %2
exit /b

:test3
echo executing :test3
echo arg1 = %1
echo arg2 = %2
echo arg3 = %3
exit /b

Я предпочитаю подход GOTO, который я использовал выше. Другой вариант - вместо этого использовать CALL, как это сделал Томас в своем ответе.

Рабочий пример полезной библиотеки пакетных функций, использующих технику CALL, см. В CHARLIB.BAT, библиотеке подпрограмм для обработки символов и строк в пакетном файле. Нить, показывающая развитие библиотеки доступна здесь

Я написал CharLib.bat несколько лет назад. Если бы я написал это сегодня, я бы, вероятно, использовал GOTO вместо CALL.

Проблема с введением CALL заключается в том, что он создает проблемы при передаче строковых литералов в качестве параметров. Дополнительный CALL означает, что строковый литерал, содержащий % проценты должны быть удвоены в дополнительное время. Это также означает, что яды без кавычек, такие как & а также | нужно будет избежать в дополнительное время. Эти две проблемы могут быть решены вызывающим абонентом. Но настоящая проблема заключается в том, что каждый CALL удваивает цитируемые значения: "^" становится "^^", Нет хорошего способа обойти проблему удвоения каретки.

Проблемы с дополнительным CALL не влияют на CharLib.bat, потому что строковые значения передаются по ссылке (имя переменной), а не как строковые литералы.

Единственным недостатком использования GOTO с SHIFT /1 является то, что вы не можете использовать %0 чтобы получить имя выполняемой в данный момент подпрограммы. Я мог бы использовать SHIFT без /1, но тогда вы не сможете использовать %~f0 внутри подпрограммы, чтобы получить полный путь к исполняемому пакетному файлу.

Я думаю, что функция маршрутизации в начале командного файла не так уж и страшна. Вы можете использовать что-то вроде этого в начале "libbatch.cmd"

    call:%*
    exit/b

:func1
    [do something]
    exit/b

:func2
    [do something else]
    exit/b

Теперь вы можете вызвать func2 из другого пакета с помощью:

call libbatch.cmd func2 params1 param2 ... paramN

это также сохраняет уровень ошибки, "выброшенный" func2 (выход /b передает текущий уровень ошибки). Вторым вызовом вместо goto вы гарантируете, что "%1"=="param1", а не func2. И вызов не прервет пакетный файл, если метка не существует, он просто устанавливает уровень ошибки в 1 и устанавливает сообщение об ошибке в 2 (errorout), которое может быть перенаправлено на nul.

Объяснение: %* содержит все параметры, поэтому в примере первая строка переводится в:

call:func2 params1 param2 ... paramN

Вы можете использовать этот формат - и запустить его так:

call mybat :function4 parameternumber2 parameternumber3

это был бы один из способов использования библиотеки

@echo off
goto %1

:function1
REM code here - recursion and subroutines will complicate the library
REM use random names for any temp files, and check if they are in use - else pick a different random name
goto :eof

:function2
REM code here - recursion and subroutines will complicate the library
REM use random names for any temp files, and check if they are in use - else pick a different random name
goto :eof

:function3
REM code here - recursion and subroutines will complicate the library
REM use random names for any temp files, and check if they are in use - else pick a different random name
goto :eof

:function4
REM code here - recursion and subroutines will complicate the library
REM use random names for any temp files, and check if they are in use - else pick a different random name
goto :eof

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

  • Библиотечные функции должны вызываться из блока кода в главном файле, и
  • В этом блоке кода никакие основные файловые функции не вызываются.

Хитрость заключается в том, чтобы "переключить контекст" запущенного пакетного файла таким образом, чтобы библиотечный файл стал запущенным пакетным файлом; таким образом, все функции в файле библиотеки становятся доступными для основного блока кода без дополнительной обработки. Конечно, "контекст" запущенного пакетного файла должен быть переключен обратно на основной файл до окончания блока кода.

Способ "переключения контекста" заключается в переименовании файла библиотеки с тем же именем запущенного основного файла (и переименовании основного файла в другое имя). Например:

(
   rem Switch the context to the library file
   ren main.bat orig-main.bat
   ren library.bat main.bat
   rem From this point on, any library function can be called
   . . . .
   rem Switch back the context to the original one
   ren main.bat library.bat
   ren orig-main.bat main.bat
)

РЕДАКТИРОВАТЬ: добавлен рабочий пример

Я скопировал приведенный ниже пример с экрана. Протестировано в Windows 8, но я также использовал этот метод в Win XP:

C:\Users\Antonio\Documents\test
>type main.bat
@echo off
(
   rem Switch the context to the library file
   ren main.bat orig-main.bat
   ren library.bat main.bat
   rem From this point on, any library function can be called, for example:
   echo I am Main, calling libFunc:
   call :libFunc param1
   echo Back in Main
   rem Switch back the context to the original one
   ren main.bat library.bat
   ren orig-main.bat main.bat
)

C:\Users\Antonio\Documents\test
>type library.bat
:libFunc
echo I am libFunc function in library.bat file
echo My parameter: %1
exit /B

C:\Users\Antonio\Documents\test
>main
I am Main, calling libFunc:
I am libFunc function in library.bat file
My parameter: param1
Back in Main

Я не уверен в контексте исходного вопроса, но это может быть случай, когда переключение на что-то вроде WSH с VBScript или WPS, или любой другой сценарий с поддержкой консоли, кроме командных файлов. Я отвечу на оригинальный вопрос, но сначала.. немного предыстории и понимания..

Режим командной строки / консоли в DOS и Windows обычно представляет собой либо COMMAND.COM, либо CMD.EXE, что не совсем подходит для логики скриптинга / программирования. Скорее, они предназначены для выполнения команд и программ, и командные файлы были добавлены к часто используемым последовательностям команд, чтобы они были объединены в одну набранную команду. Например, у вас может быть старая игра для DOS, в которую вы играете, для которой каждый раз нужны следующие команды, поэтому она упакована в пакетный файл:

@EHO OFF
@REM Load the VESA driver fix..
VESAFIX.EXE
@REM Load the joystick driver..
JOYSTICK.COM
@REM Now run the game
RUNGAME.EXE

Многие люди склонны рассматривать весь пакетный файл как единое целое - но это не так. Интерпретатор команд (COMMAND.COM или CMD.EXE) будет действовать так, как если бы вы вручную вводили эти строки, одну за другой, каждый раз, когда запускаете командный файл. У него действительно нет четкой концепции лексики и области видимости, как у обычного языка программирования / написания сценариев, то есть он не поддерживает много дополнительных метаданных, таких как стек вызовов и так далее. То, что он поддерживает, больше добавляется как запоздалая мысль, а не как встроенный в пакетный файл с самого начала.

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

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

CALL BATLIB.BAT FunctionName Parameter1 Parameter2 ...

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

Использование более современной версии CMD.EXE в системах Windows позволяет использовать ": метки" в синтаксисе CALL, что может быть полезно, если вы хотите ограничить область действия параметра (что позволяет использовать%* для "всех аргументов"). например), вот так:

CALL :LABEL Parameter1 Paramater2 ...

(из одного пакетного файла или...)

CALL BATLIB.BAT :LABEL Parameter1 Parameter2 ...

Несколько замечаний по этому поводу, хотя... В первой форме:LABEL должен быть уже в текущем пакетном файле. Это создаст новый "пакетный контекст" в CMD.EXE, где%*, %1, %2 и так далее сопоставляются с параметрами. Но вы также должны будете предоставить какую-то логику возврата / выхода для возврата / выхода из этого контекста обратно в контекст вызова.

Во второй форме CMD.EXE на самом деле не распознает, что вы передаете ему метку, поэтому вашей библиотеке пакетных файлов придется ожидать ее и обрабатывать:

@ECHO OFF
CALL %*

Это работает, потому что интерпретатор команд заменяет%* еще до того, как попытается проанализировать команду CALL, поэтому после расширения переменной команда CALL увидит:LABEL, как если бы он был жестко закодирован. Это также создает ситуацию, когда CMD.EXE создает еще один пакетный контекст, так что вам придется обязательно возвращаться / выходить из этого контекста дважды: один раз для текущего библиотечного контекста, снова, чтобы вернуться к исходному CALL.

Есть и другие способы создания библиотеки пакетных файлов, смешивания и сопоставления вышеуказанных методов или использования еще более сложной логики с использованием GOTO и так далее. На самом деле это настолько сложная тема, что на эту тему написаны целые разделы книг, гораздо больше, чем я хочу напечатать в простом ответе!

И до сих пор я в основном игнорировал другие проблемы, с которыми вы столкнетесь: что, если метка CALL не существует? Как это будет обработано? Как насчет расширения переменных среды? когда это происходит? Как вы можете предотвратить это слишком рано? Как насчет использования специальных символов DOS в аргументах / параметрах? Например, как интерпретатор видит строку вроде: CALL:ProcessPath %PATH%? (Ответ на этот вопрос заключается в том, что CMD.EXE _ заменяет весь % PATH% до того, как _ он даже обрабатывает команду CALL. Это может создать проблемы, если в вашем пути есть пробелы, которые могут привести к путанице в том, как CALL обрабатывает всю вещь, так много Переменные% PATH% в Windows делают.. C:\Program Files.. например..)

Как вы можете видеть, все усложняется и запутывается очень быстро... И вы должны перестать думать как программист и начать думать как COMMAND.COM/CMD.EXE, который в значительной степени видит только одну строку за раз, а не Весь пакетный файл в виде атомной единицы. На самом деле, вот пример, который поможет вам понять, как это работает..

Создайте папку C:\testing и поместите в нее следующий пакетный файл с именем "oops.bat":

@ECHO OFF
ECHO Look mom, no brain!
PAUSE
ECHO Bye mom!

Теперь откройте окно консоли и запустите его, но позвольте ему сидеть в ПАУЗЕ:

C:\testing>oops.bat
Look mom, no brain!
Press any key to continue . . .

Пока он находится в PAUSE, откройте oops.bat в вашем текстовом редакторе и измените его на:

@ECHO OFF
ECHO Look mom, no brain!?
ECHO Oops!
PAUSE
ECHO Bye mom!

Сохраните его, затем переключитесь обратно в окно консоли и нажмите любую клавишу, чтобы продолжить запуск командного файла:

'ops!' is not recognized as an internal or external command,
operable program or batch file.
Press any key to continue . . .
Bye mom!
c:\testing>

Вау.. видите эту ошибку там? Это произошло из-за того, что мы отредактировали пакетный файл, пока он еще запускался CMD.EXE, но наши изменения изменились в том месте, где, как предполагал CMD.COM в пакетном файле. Внутри CMD.EXE поддерживает указатель файла, указывающий начало следующего символа для обработки, который в этом случае был бы байтом сразу после строки с PAUSE (и CRLF) на нем. Но когда мы отредактировали его, он изменил расположение следующей команды в командном файле, но указатель CMD.EXE остался на том же месте. В этом случае он указывал на позицию байта прямо в середине "ECHO Oops!" линии, поэтому он попытался обработать "ops!" как команда после паузы.

Я надеюсь, это ясно дает понять, что COMMAND.COM/CMD.EXE всегда будет видеть ваши командные файлы как поток байтов, а не как логический блок, подпрограммы и так далее, как это делал бы язык сценариев или компилятор. Вот почему библиотеки пакетных файлов так ограничены. Это делает невозможным "импорт" библиотеки в текущий запущенный пакетный файл.

О, и у меня просто была другая мысль... В современном Windows CMD.EXE вы всегда можете создать пакетный файл, который на лету создает временный пакетный файл, а затем вызывает его:

@ECHO OFF
SET TEMPBAT=%TEMP%\TMP%RANDOM:~0,1%%RANDOM:~0,1%%RANDOM:~0,1%%RANDOM:~0,1%.BAT
ECHO @ECHO OFF > %TEMPBAT%
ECHO ECHO Hi Mom! I'm %TEMPBAT%! >> %TEMPBAT%
ECHO Hello, world, I'm %~dpnx0!
CALL %TEMPBAT%
DEL %TEMPBAT%

Это фактически создает временный пакетный файл во временном каталоге с именем TMP####.BAT (где # заменяются случайными числами;%RANDOM:~0,1% означает, что берется первая цифра возвращенного числа %RANDOM%- мы хотели, чтобы здесь была только одна единственная цифра, а не полное число, которое возвращает RANDOM..), затем ECHO "Hello, World", за которым следует его собственное полное имя (часть%~dpnx0), ВЫЗЫВАЕТ временный пакетный файл, который, в свою очередь, ECHO "Привет, мама!" за ним следует собственное [случайное] имя, а затем возвращается к исходному пакетному файлу, чтобы он мог выполнять любые необходимые операции очистки, например, удаление временного пакетного файла в этом случае.

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

Выполните быстрый поиск в Google для "ПРОГРАММИРОВАНИЯ ФАЙЛОВ ФАЙЛОВ", чтобы найти многие из них, и вы также можете проверить Wiki и WikiBooks, SS64.com, robvanderwoude.com и даже http://www.dmoz.org/Computers/Software от DMOZ. http://www.dmoz.org/Computers/Software/Operating_Systems/x86/DOS/Programming/Languages/Batch/ каталог с большим количеством ресурсов.

Удачи!

Вот сценарий пакетной обработки cmd, который я написал, который импортирует файлы или файлы в папках (рекурсивно) в основной сценарий:

@echo off
REM IMPORT - a .cmd utility for importing subroutines into the main script
REM Author: Ioan Marin

REM !!! IN ORDER TO FUNCTION CORRECTLY:                                                       !!!
REM !!! IMPORT MUST BE CALLED INSIDE A DISABLED DELAYED EXPANSION BLOCK/ENVIRONMENT (DEFAULT) !!!


    rem \\// Define import file mask here:
    rem If mask is not defined outside "import.cmd":
    if not defined mask (
        set "mask=*.cmd; *.bat"
    )
    rem //\\ Define import file mask here:

    rem Detect if script was started from command line:
    call :DetectCommandLine _not_started_from_command_line

    if "%~1" == "/install" (
        set "import_path=%~dp0"
        call :EscapePathString import_path import_path_escaped
    )

    if not "%~1" == "" (
        if /i not "%~1" == "end" (
            if "%~1" == "/?" (
                call :DisplayHelp
            ) else (
                if "%~1" == "/install" (
                    echo Installing
                    set "_first_time="

                    rem This should get into the Autorun registry key: path %path%;"...\import.cmd"
                    rem     If you want, other commands can be added to the left or to the right of this command, unified as a block with a "&" command operator
                    REG ADD "HKCU\Software\Microsoft\Command Processor" /v AutoRun /t REG_SZ /d "path %%path%%;"""%import_path_escaped%""||(
                        echo ERROR: Cannot install import: cannot write to the registry^!
                        echo You can try to manually add the "import.cmd" path in the "PATH" variable or use pushd ^(see help^).
                        if not "%_not_started_from_command_line%" == "0" (
                            call :PressAnyKey Press any key to exit...
                            echo.
                        )
                        exit /b 1
                    )

                    echo.
                    echo Done. The install directory was set to: 
                    echo "%import_path%"
                    echo and was added to the PATH environment variable. You can add other desired programs into this directory.
                    echo.
                    echo Please note that the console needs to be closed and reopened in order for the changes to take effect^!
                    echo.
                ) else (
                    if not defined _first_time (
                        set _first_time=defined
                        set /a count=0
                        if "%_DISPLAY_WARNING%" == "true" (
                            echo.
                            echo WARNING: CMD_LIBRARY was reset to "", because previously it gave an error^!
                            echo.
                        )
                        echo Loading list to import...
                    )
                    REM build import files list 

                    set /a count+=1
                    call set "_import_list_%%count%%=%%~1"
                )
            )
        )
    ) else (
        call :DisplayHelp
    )

    if /i "%~1" == "end" (

        set "_first_time="

        echo Done.
        echo Analyzing...

        rem set "_main_program=%~dpnx2"

        if not exist "%~dpnx2" (
            echo ERROR: Second parameter, after "import end", must be a valid file path - see help^!>>&2

            rem Clean up
            call :CleanUp

            if not "%_not_started_from_command_line%" == "0" (
                call :PressAnyKey Press any key to exit...
                echo.
            )
            exit /b 1
        )
    )

    if /i "%~1" == "end" (
        set "_main_batch_script=%~dpnx2"

        rem \\// Define output filename here:
        rem set "_output_filename=tmp0001_%~n2.cmd"
        set "_output_filename=tmp0001.cmd"
        rem //\\ Define output filename here:
    )

    if /i "%~1" == "end" (
        rem Check all paths not to be UNC:
        setlocal EnableDelayedExpansion
            set "_error=false"

            call :TestIfPathIsUNC _main_batch_script _result
            if "!_result!" == "true" (
                set "_error=true"
                echo. 
                echo ERROR: UNC paths are not allowed: Second parameter, after "import end", must not be a UNC path^^^! Currently it is: "!_main_batch_script!">>&2
            )

            set "_CMD_LIBRARY_error=false"
            call :TestIfPathIsUNC CMD_LIBRARY _result
            if "!_result!" == "true" (
                set "_error=true"
                set "_CMD_LIBRARY_error=true"
                echo.
                echo ERROR: UNC paths are not allowed: CMD_LIBRARY variable must not contain a UNC path^^^! Currently, it is set to: "!CMD_LIBRARY!".>>&2
            )

            for /l %%i in (1,1,!count!) do (
                call :TestIfPathIsUNC _import_list_%%i _result
                if "!_result!" == "true" (
                    set "_error=true"
                    echo.
                    echo ERROR: UNC paths are not allowed: The import path: "!_import_list_%%i!" is a UNC path^^^!>>&2
                )
            )

            if "!_error!" == "true" (
                echo.
                echo Errors were ecountered^^^!

                if "!_CMD_LIBRARY_error!" == "true" (
                    endlocal
                    set "_CMD_LIBRARY_error=true"
                ) else (
                    endlocal
                    set "_CMD_LIBRARY_error=false"
                )

                rem Clean up
                call :CleanUp

                if not "%_not_started_from_command_line%" == "0" (
                    call :PressAnyKey Press any key to exit...
                    echo.
                )
                exit /b 1
            ) else (
                endlocal
                set "_CMD_LIBRARY_error=false"
            )
    )

    if /i "%~1" == "end" (
        rem Check all paths not to contain "*" and "?" wildcards:
        set "_asterisk=*"
        set "_qm=?"

        setlocal EnableDelayedExpansion
            set "_error=false"

            call :TestIfStringContains _main_batch_script _asterisk _result1
            call :TestIfStringContains _main_batch_script _qm _result2
            if "!_result1!" == "true" (
                set "_error=true"
            )
            if "!_result2!" == "true" (
                set "_error=true"
            )
            if "!_error!" == "true" (
                echo. 
                echo ERROR: The use of "*" or "?" wildcards is not supported by import: Second parameter, after "import end", must not contain "*" or "?" wildcards^^^! Currently it is: "!_main_batch_script!". Instead, you can set the mask with a set of name and extension masks sepparated by semicolon, like: set mask="*.cmd; *.bat">>&2
            )

            set "_CMD_LIBRARY_error=false"
            call :TestIfStringContains CMD_LIBRARY _asterisk _result1
            call :TestIfStringContains CMD_LIBRARY _qm _result2
            if "!_result1!" == "true" (
                set "_error=true"
            )
            if "!_result2!" == "true" (
                set "_error=true"
            )
            if "!_error!" == "true" (
                set "_error=true"
                set "_CMD_LIBRARY_error=true"
                echo.
                echo ERROR: The use of "*" or "?" wildcards is not supported by import: CMD_LIBRARY variable must not contain "*" or "?" wildcards^^^! Currently, it is set to: "!CMD_LIBRARY!". Instead, you can set the mask with a set of name and extension masks sepparated by semicolon, like: set mask="*.cmd; *.bat">>&2
            )

            for /l %%i in (1,1,!count!) do (
                call :TestIfStringContains _import_list_%%i _asterisk _result1
                call :TestIfStringContains _import_list_%%i _qm _result2
                if "!_result1!" == "true" (
                    set "_error=true"
                )
                if "!_result2!" == "true" (
                    set "_error=true"
                )
                if "!_error!" == "true" (
                    set "_error=true"
                    echo.
                    echo ERROR: The use of "*" or "?" wildcards is not supported by import: The import path: "!_import_list_%%i!" must not contain "*" or "?" wildcards^^^! Instead, you can set the mask with a set of name and extension masks sepparated by semicolon, like: set mask="*.cmd; *.bat">>&2
                )
            )

            if "!_error!" == "true" (
                echo.
                echo Errors were ecountered^^^!

                if "!_CMD_LIBRARY_error!" == "true" (
                    endlocal
                    set "_CMD_LIBRARY_error=true"
                ) else (
                    endlocal
                    set "_CMD_LIBRARY_error=false"
                )

                rem Clean up
                call :CleanUp

                if not "%_not_started_from_command_line%" == "0" (
                    call :PressAnyKey Press any key to exit...
                    echo.
                )
                exit /b 1
            ) else (
                endlocal
                set "_CMD_LIBRARY_error=false"
            )
    )

    if /i "%~1" == "end" (
        pushd "%~dp2"
            call set "_output_dir=%%CD%%"
        popd
    )

    if /i "%~1" == "end" (

        if not defined CMD_LIBRARY (

            set CMD_LIBRARY_CASE=IMPORT.CMD

            set "BASE=%~dpnx0\.."

            pushd "%~dpnx0"\..\

                REM \\// Define CMD LIBRARY here ("." is relative to "import.cmd" parent directory):
                REM if CMD_LIBRARY is not defined outside import.cmd, "." (used here) is related to import.cmd parent directory:
                set "CMD_LIBRARY=."
                REM //\\ Define CMD LIBRARY here ("." is relative to "import.cmd" parent directory):

        ) else (

            set CMD_LIBRARY_CASE=MAIN.CMD

            set "BASE=%~dpnx2\.."

            REM if CMD_LIBRARY is defined outside the "import.cmd" script, "." (used in CMD_LIBRARY) is related to "main program" parent directory
            pushd "%~dpnx2"\..

        )
    )

    if /i "%~1" == "end" (

        call :DeQuoteOnce CMD_LIBRARY CMD_LIBRARY
        call set "CMD_LIBRARY_ORIGINAL=%%CMD_LIBRARY%%"

        call :TestIfPathIsUNC CMD_LIBRARY_ORIGINAL _result
        setlocal EnableDelayedExpansion
            if "!_result!" == "true" (
                set "_error=true"

                echo.
                echo ERROR: UNC paths are not allowed: CMD_LIBRARY variable must not contain a UNC path^^^! Currently, it is set to: "!CMD_LIBRARY_ORIGINAL!".>>&2

                echo.
                echo Errors were ecountered^^^!

                endlocal
                set "_CMD_LIBRARY_error=true"

                rem Clean up
                call :CleanUp

                if not "%_not_started_from_command_line%" == "0" (
                    call :PressAnyKey Press any key to exit...
                    echo.
                )
                exit /b 1
            ) else (
                endlocal
                set "_CMD_LIBRARY_error=false"
            )

        call pushd "%%CMD_LIBRARY%%" >nul 2>nul&&(
            call set "CMD_LIBRARY=%%CD%%"
        )||(
            call echo ERROR: Could not access directory CMD_LIBRARY=^"%%CMD_LIBRARY%%^"^!>>&2

            call :CleanUp

            if not "%_not_started_from_command_line%" == "0" (
                call :PressAnyKey Press any key to exit...
                echo.
            )
            popd
            exit /b 1
        )

    )

    if /i "%~1" == "end" (
        setlocal EnableDelayedExpansion
            set _error=false
            pushd "!BASE!"
            echo.
            if "!CMD_LIBRARY_CASE!" == "IMPORT.CMD" (
                echo CMD_LIBRARY was defined as: "!CMD_LIBRARY_ORIGINAL!" in the file "import.cmd" and was expanded to: "!CMD_LIBRARY!"
            ) else (
                if "!CMD_LIBRARY_CASE!" == "MAIN.CMD" (
                    echo CMD_LIBRARY was defined as: "!CMD_LIBRARY_ORIGINAL!" outside "import.cmd" file "%~nx2" and was expanded to: "!CMD_LIBRARY!"
                ) 
            )
                for /l %%i in (1,1,!count!) do (
                    if not exist "!_import_list_%%i!" (
                        if not exist "!CMD_LIBRARY!\!_import_list_%%i!" (
                            rem if first time:
                            if not "!_error!" == "true" (
                                echo.
                                echo Directory of "!CMD_LIBRARY!":
                            )

                            echo.
                            echo ERROR: element "!_import_list_%%i!" does not exist or is not accessible as a standalone file/dir or as a file/dir in the directory contained by "CMD_LIBRARY" variable^^^!>>&2
                            set _error=true
                        )
                    )
                )
            popd
            if "!_error!" == "true" (
                endlocal

                rem Clean up
                call :CleanUp

                if not "%_not_started_from_command_line%" == "0" (
                    call :PressAnyKey Press any key to exit...
                    echo.
                )
                exit /b 1
            ) else (
                endlocal
            )
        echo OK
        echo.

    )

    set "_error=false"
    if /i "%~1" == "end" (

        echo Output file is: "%_output_dir%\%_output_filename%"
        echo.
        echo Importing...
        echo.
        (
            type nul>"%_output_dir%\%_output_filename%"
        ) 2>nul||(
            echo ERROR: Could not write to file: "%_output_dir%\%_output_filename%"^!>>&2

            rem Clean up
            call :CleanUp

            if not "%_not_started_from_command_line%" == "0" (
                call :PressAnyKey Press any key to exit...
                echo.
            )
            exit /b 1
        )

        echo Importing main script "%_main_batch_script%"
        (
            echo @set _import=defined
            echo @REM Timestamp %date% %time%

            echo.
        )>>"%_output_dir%\%_output_filename%"
        (
            (
                type "%_main_batch_script%"
            )>>"%_output_dir%\%_output_filename%"
        ) 2>nul||(echo  ERROR: Could not read file^!&set "_error=true">>&2)
        (
            echo.
            echo.
        )>>"%_output_dir%\%_output_filename%"
        echo.

        echo Directory of "%CMD_LIBRARY%":
        if not "%CMD_LIBRARY_CASE%" == "MAIN.CMD" (
            pushd "%BASE%"
        )
        if not defined mask (
            rem If mask is not defined, import all file types:
            set "mask=*"
        )
        for /l %%i in (1,1,%count%) do (
            call set "_import_list_i=%%_import_list_%%i%%"
            call :ProcedureImportCurrentFile
        )
        if not "%CMD_LIBRARY_CASE%" == "MAIN.CMD" (
            popd
        )
    )

    if "%~1" == "end" (
        if "%_error%" == "true" (
            echo.
            echo Errors were ecountered^!

            rem Clean up
            call :CleanUp

            if not "%_not_started_from_command_line%" == "0" (
                call :PressAnyKey Press any key to exit...
                echo.
            )
            exit /b 1
        ) else (
            echo Done^!
        )

        call popd
        popd

        rem Clean up
        call :CleanUp

        rem Detect if script was started from command line:
        call :DetectCommandLine _not_started_from_command_line
    )
    if "%~1" == "end" (
        if "%_not_started_from_command_line%" == "0" (
            set "_import="
        ) else (
            echo.
            echo Starting program...
            echo.
            rem Start "resulting" program:
            "%_output_dir%\%_output_filename%"
        )
    )

goto :eof

REM \\\/// Next subroutines use jeb's syntax for working with delayed expansion: \\\///

:CleanUp
(   
    setlocal EnableDelayedExpansion
)
(
    endlocal

    if "%_CMD_LIBRARY_error%" == "true" (
        set "CMD_LIBRARY="
        set "_DISPLAY_WARNING=true"
    ) else (
        set "_DISPLAY_WARNING=false"
    )
    set "_first_time="
    for /l %%i in (1,1,%count%) do (
        set "_import_list_%%i="
    )
    rem optional:
    set "count="
    set "import_path="
    rem set "_output_dir="
    set "_error="
    set "_main_batch_script="
    rem set "_output_filename="
    rem set "_import="
    set "mask="

    exit /b
)

:GetStrLen - by jeb - adaptation
(   
    setlocal EnableDelayedExpansion
        set "s=!%~1!#"
        set "len=0"
        for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
            if "!s:~%%P,1!" NEQ "" ( 
                set /a "len+=%%P"
                set "s=!s:~%%P!"
            )
        )
)
( 
    endlocal
    set "%~2=%len%"
    exit /b
)

:EscapePathString

(   
    setlocal EnableDelayedExpansion
        set "string=!%~1!"
        call :GetStrLen string string_len
        set /a string_len-=1

        for /l %%i in (0,1,!string_len!) do (
            rem escape "^", "(", ")", "!", "&"
            if "!string:~%%i,1!" == "^" (
                set "result=!result!^^^^"
            )  else (
                if "!string:~%%i,1!" == "(" (
                    set "result=!result!^^^("
                ) else (
                    if "!string:~%%i,1!" == ")" (
                        set "result=!result!^^^)"
                    ) else (
                        if "!string:~%%i,1!" == "^!" (
                            set "result=!result!^^^!"
                        ) else (
                            if "!string:~%%i,1!" == "&" (
                                set "result=!result!^^^&"
                            ) else (
                                if "!string:~%%i,1!" == "%%" (
                                    set "result=!result!%%"
                                ) else (
                                    set "result=!result!!string:~%%i,1!"
                                )
                            )
                        )
                    )
                )
            )
        )
)
(
    endlocal
    set "%~2=%result%"
    exit /b
)

:PressAnyKey
    set /p=%*<nul
    pause>nul
goto :eof

:DeQuoteOnce
(
    setlocal EnableDelayedExpansion
        set "string=!%~1!"
        if "!string!" == """" (
            endlocal
            set "%~2=%string%"
            exit /b
        )
        rem In order to work with " we replace it with a special character like < > | that is not allowed in file paths:
        set "string=!string:"=^<!"

        if "!string:~0,1!" == "<" (
            if "!string:~-1,1!" == "<" (
                set "string=!string:~1,-1!"
            )
        )
        rem restore " in string (replace < with "):
        set "string=!string:<="!"
)
(
    endlocal
    set "%~2=%string%"
    exit /b
)

:TestIfPathIsUNC

(
    setlocal EnableDelayedExpansion
        set "_current_path=!%~1!"
        set "_is_unc_path=true"
        if defined _current_path (
            if "!_current_path:\\=!" == "!_current_path!" (
                set "_is_unc_path=false"
            )
        ) else (
            set "_is_unc_path=false"
        )
)
(
    endlocal
    set "%~2=%_is_unc_path%"
    exit /b
)

:TestIfStringContains

(
    setlocal EnableDelayedExpansion
        echo "!%~1!"|find "!%~2!">nul 2>nul
        set "_error_code=!ERRORLEVEL!"
)
(
    endlocal
    if "%_error_code%" == "0" (
        set "%~3=true"
    ) else (
        set "%~3=false"
    )
    exit /b
)

REM ///\\\ The subroutines above use jeb's syntax for working with delayed expansion: ///\\\

:DetectCommandLine

setlocal
    rem Windows: XP, 7
    for /f "tokens=*" %%c in ('echo "%CMDCMDLINE%"^|find "cmd /c """ /c') do (
        set "_not_started_from_command_line=%%~c"
    )
    if "%_not_started_from_command_line%" == "0" (
        rem Windows: 10
        for /f "tokens=*" %%c in ('echo "%CMDCMDLINE%"^|find "cmd.exe /c """ /c') do (
            set "_not_started_from_command_line=%%~c"
        )
    )
endlocal & (
    set "%~1=%_not_started_from_command_line%"
)
goto :eof

:ProcedureImportCurrentFile

setlocal
    set "cc="

    if not exist "%_import_list_i%" (
        set "_not_a_dir=false"
        pushd "%CMD_LIBRARY%\%_import_list_i%\" 1>nul 2>&1||set "_not_a_dir=true"
        call :GetStrLen CD _CD_len
    )
    if "%_not_a_dir%" == "false" (
        setlocal EnableDelayedExpansion
            if not "!CD:~-1,1!" == "\" (
                endlocal
                set /a _CD_len+=1
            ) else (
                endlocal
            )
        popd
    )

    if not exist "%_import_list_i%" (
        if "%_not_a_dir%" == "true" (
            echo Importing file "%CMD_LIBRARY%\%_import_list_i%"
            (
                type "%CMD_LIBRARY%\%_import_list_i%">>"%_output_dir%\%_output_filename%"
            ) 2>nul||(
                echo  ERROR:   Could not read file^!>>&2
                set "_error=true"
            )
            (
                if not "%%i" == "%count%" (
                    echo.
                    echo.
                ) else (
                    echo.
                )
            )>>"%_output_dir%\%_output_filename%"
        ) else (
            echo Importing dir "%_import_list_i%"
            rem
            pushd "%CMD_LIBRARY%\%_import_list_i%\"

                set /a cc=0
                for /r %%f in (%mask%); do (
                    set "_current_file=%%~dpnxf"
                    call set "r=%%_current_file:~%_CD_len%%%"
                    call echo   Importing subfile "%%_import_list_i%%\%%r%%"
                    (
                        (
                            call type "%%_current_file%%"
                        )>>"%_output_dir%\%_output_filename%"
                    ) 2>nul||(
                        echo     ERROR: Could not read file^!>>&2
                        set "_error=true"
                    )
                    (
                        echo. 
                        echo. 
                    )>>"%_output_dir%\%_output_filename%"
                    set /a cc+=1
                )
                popd
        )
    ) else (
        set "_not_a_dir=false"
        pushd "%_import_list_i%\" 1>nul 2>&1||set "_not_a_dir=true"
        call :GetStrLen CD _CD_len
    )
    if "%_not_a_dir%" == "false" (
        setlocal EnableDelayedExpansion
            if not "!CD:~-1,1!" == "\" (
                endlocal
                set /a _CD_len+=1
            ) else (
                endlocal
            )
        popd
    )

    if exist "%_import_list_i%" (
        if "%_not_a_dir%" == "true" (
            echo Importing file "%_import_list_i%"
            (
                type "%_import_list_i%">>"%_output_dir%\%_output_filename%"
            ) 2>nul||(
                echo    ERROR: Could not read file^!>>&2
                set "_error=true"
            )
            (
                if not "%%i" == "%count%" (
                    echo.
                    echo.
                ) else (
                    echo.
                )
            )>>"%_output_dir%\%_output_filename%"
        ) else (
            rem
            echo Importing dir "%_import_list_i%"
            pushd "%_import_list_i%\"

            set /a cc=0
            for /r %%f in (%mask%); do (
                set "_current_file=%%~dpnxf"
                call set "r=%%_current_file:~%_CD_len%%%"
                call echo   Importing subfile "%%_import_list_i%%\%%r%%"
                (
                    (
                        call type "%%_current_file%%"
                    )>>"%_output_dir%\%_output_filename%"
                ) 2>nul||(
                    echo     ERROR: Could not read file^!>>&2
                    set "_error=true"
                )
                (
                    echo. 
                    echo. 
                )>>"%_output_dir%\%_output_filename%"
                set /a cc+=1
            )
            popd
        )
    )
    if "%cc%" == "0" (
        echo   No match^!
    )
endlocal & (
    set "_error=%_error%"
)
goto :eof

:DisplayHelp
    echo IMPORT - a .cmd utility for importing subroutines into the main script
    echo.
    echo NOTES: 1. This utility assumes that command extensions are enabled (default) and that delayed expansion can be enabled;
    echo           ALSO IMPORT MUST BE CALLED INSIDE A DISABLED DELAYED EXPANSION BLOCK/ENVIRONMENT (DEFAULT);
    echo           These are necessary in order for it to function correctly.
    echo        2. The use of UNC paths is not supported by import. As a workarround, you can mount a UNC path to a temporary drive using "pushd".
    echo           The use of "*" or "?" wildcards is not supported by import. Instead, you can set the mask with a set of name and extension masks sepparated by semicolon, like: set mask="*.cmd; *.bat"
    echo           When the "mask" variable is set, only the filenames having the extensions contained by it are matched at import.
    echo.
    echo Description:
    echo    import organizes your batch programs on common libraries of subroutines, that you can use in the future for other programs that you build; it also makes code editing and debugging easier. 
    echo.
    echo Usage [1]:
    echo    import [flags]
    echo.
    echo    [flags] can be:
    echo            /install - installs import into the registry, in the Command Processor AutoRun registry key ^(adds the current location of import into the PATH variable^).
    echo            /? - displays help ^(how to use import^)
    echo.
    echo Usage [2]:
    echo    What it does:
    echo            Concatenates ^(appends^) files content containing subroutines to the main program content using the following SYNTAX:
    echo            REM \\//Place this in the upper part of your script ^(main program)^ \\//:
    echo.
    echo @echo off
    echo.
    echo            if not defined _import ^(
    echo                            rem OPTIONAL ^(before the "import" calls^):
    echo                            set "CMD_LIBRARY=^<library_directory_path^>"
    echo.
    echo                    import "[FILE_PATH1]filename1" / "DIR_PATH1"
    echo                    ...
    echo                    import "[FILE_PATHn]filenamen" / "DIR_PATHn"
    echo                    import end "%%~0"
    echo            ^)
    echo.
    echo            REM //\\Place this in the upper part of your script ^(main program)^ //\\:
    echo.
    echo            "filename1" .. "filenamen" represent the filenames that contain the subroutines that the user wants to import in the current ^(main^) program. The paths of these files are relative to the directory contained in the CMD_LIBRARY variable.
    echo.
    echo            "FILE_PATH1" .. "FILE_PATHn" represent the paths of these files.
    echo.
    echo            "DIR_PATH1" .. "DIR_PATHn" represent directories paths in which to recursivelly search and import all the files of the type defined in the variable "mask"
    echo.
    echo            CMD_LIBRARY is a variable that contains the directory path where your library of files ^(containing subroutines^) is found.
    echo.
    echo            We denote the script that calls "import" as "the main script".
    echo.
    echo            By default, if not modified in outside the import.cmd script, in the import.cmd script - CMD_LIBRARY is set to "." directory and is relative to the "import.cmd" parent directory.
    echo            If CMD_LIBRARY directory is modified outside the import.cmd script, CMD_LIBRARY is relative to the main script parent directory.
    echo.
    echo            Note that only the last value of "CMD_LIBRARY" encountered before `import end "%%~0"` is taken into consideration.
    echo.
    echo            import end "%%~0" - marks the ending of importing files and the start of building of the new batch file ^(named by default tmp0001.cmd, and located in the directory in which the main script resides^).
    echo.
    echo            "%%~0" represents the full path of the main script.
    echo.
    echo    Author: Ioan Marin
goto :eof

Чтобы использовать это:

  • сохраните это как import.cmd

  • назовите это с /install флаг для его установки (не требует админа)

  • добавьте такой заголовок в начале вашего основного скрипта, который вызывает подпрограммы, которые хранятся в других файлах - файлах, которые будут импортированы:

     if not defined _import (
             rem OPTIONAL (before the "import" calls):
             set "CMD_LIBRARY=<library_directory_path>"
    
         import "[FILE_PATH1]filename1" / "DIR_PATH1"
         ...
         import "[FILE_PATHn]filenamen" / "DIR_PATHn"
         import end "%~0"
     )
    

Чтобы узнать, как его использовать, просто вызовите его с /? флаг.

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