如何将所有的函数打包成一个批处理文件并保存为单独的文件?

13

我的问题与这个问题有关。我有几个需要从批处理文件中执行的动作,我希望将它们建模为函数,并从主序列中调用。从上面的问题中,可以清楚地看出我可以使用调用语法来实现这一点。

call:myDosFunc
我的问题是:我能否将所有这些函数放在一个独立的批处理文件(functions.bat)中,并以某种方式在主批处理文件中'include'它并调用它们?另一个选项是利用从main.bat调用functions.bat的可能性,使用call语法,但我不确定是否可以调用特定的函数而不是执行整个批处理文件。
简而言之,我正在寻找类似于C编程世界的东西,在那里我的函数驻留在DLL中,主程序仅包含高级逻辑,并从DLL中调用函数。

1
你可以这样做,但这不是一个好的解决方案,它很混乱,让你的脚本处于库的控制之下,而且各种脚本都试图同时使用该库时可能会发生争用。因此,你需要编写库来处理并发访问。在我看来,有经验的批处理脚本编写者倾向于编写自包含的脚本,而不依赖于单个库,但会使用多个可作为实用程序的批处理文件。 - foxidrive
@foxidrive:感谢您的反馈。请看我对DrakaSAN的最后回复。我现在没有重新设计的位置。 - PermanentGuest
@dbenham 当你想要广泛使用递归时,它也会崩溃和烧毁,如果你的库还使用了50个子程序 - 祝你好运维护它。 :) - foxidrive
1
@foxidrive - 我并不经常使用库脚本,但是我确实没有遇到过任何问题。我同意你提出的问题是批处理编程的普遍关注点,但并不特定于构建函数库。我只是认为尽可能地隔离问题并不会“搅浑水”。我现在就停止 :) - dbenham
1
@PermanentGuest - 关于你在批处理中使用互斥锁的问题:请参考如何在Windows下共享日志文件?通过Powershell或BATCH序列化symstore的执行这两个示例,展示了如何在并行批处理过程中序列化事件。 - dbenham
显示剩余10条评论
6个回答

12

我认为在批处理文件开头使用路由函数并不丑陋。 您可以在 "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引发的错误级别(exit/b将当前错误级别传递)。

通过第二次调用而不是使用goto,您确保“%1”==“param1”,而不是func2。如果标签不存在,调用不会终止批处理文件,它只是将错误级别设置为1并将错误消息放入2(errorout),可以将其重定向到nul。

解释:%*包含所有参数,因此在此示例中,第一行翻译为:

call:func2 params1 param2 ... paramN

这变得越来越好了。感谢您的建议。 - PermanentGuest
还有一件事..我不知道路由可以像这里或上面的答案中所示那样进行“动态”处理。这模拟了C++中的虚函数调用,这并不坏。我一直在考虑一系列if-else语句来实现它。 - PermanentGuest
2
最好使用GOTO而不是CALL,因为CALL可能会破坏参数:%字符会丢失,并且调用者必须将其加倍以进行补偿。引用的^字符由CALL加倍。^加倍是一个令人讨厌的问题。我不知道有什么好的解决方案。 - dbenham

8
这里有一个简单的例子,展示了如何实现它。
函数脚本以函数名称作为第一个参数调用,并将函数参数作为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,就像Thomas在他的答案中所做的那样。

一个使用CALL技术的有用批处理函数库的工作示例,请参见CHARLIB.BAT,这是一个用于在批处理文件中处理字符和字符串的例程库。可以在此处找到展示该库开发的线程。

我几年前写了CharLib.bat。如果我今天要写它,我可能会使用GOTO而不是CALL。

引入CALL的问题在于,当将字符串字面值作为参数传递时会出现问题。额外的CALL意味着包含%的字符串字面值必须再次加倍百分号。它还意味着未加引号的毒瘤字符如&|需要再次转义。这两个问题可以由调用者解决。但是真正的问题在于每个CALL会使带引号的插入符加倍:"^"变成了"^^"。没有很好的方法解决插入符加倍的问题。

额外CALL的问题对CharLib.bat没有影响,因为字符串值是通过引用(变量名)而不是作为字符串字面值传递的。

使用SHIFT /1和GOTO的唯一缺点是您无法使用%0来获取当前正在执行的例程的名称。我可以使用未带/1的SHIFT,但这样您将无法在例程中使用%~f0获取执行批处理文件的完整路径。


真棒,dbenham。感谢你的帮助。我之前有一个想法,可以通过一系列 if-else 语句完成每个函数的操作,虽然不太美观,但也能实现。但这种方式更加简洁优雅。我之前不知道 goto 可以使用参数。非常感谢。 - PermanentGuest
过于复杂,请参考@Thomas的答案。 - Sandeep Datta
1
@SandeepDatta-哇,你和我的投票时间点确实有所不同。你可能认为这个代码过于复杂,但是这个额外的代码提供了两个非常有用的功能:1)错误检查以确保正确调用具有有效例程的“库”,2)SHIFT操作使得可以逐字复制现有子例程并将其合并到库中,而无需调整参数编号。你可能认为这些功能并不值得,但因为它们被包含在内而进行投票是严厉的。Thomas解决方案使用CALL可能会破坏参数。 - dbenham
很抱歉你有这样的感受。这是一个客观的决定。你的解决方案对我并不起作用,现在我不能在子程序中使用%*。然而,Thomas的解决方案确实有效。如果我先看到他的解决方案,可能就能节省些时间和挫折了。你已经拥有30K多的声望,一个反对票并不会对你造成伤害。 - Sandeep Datta

3
您可以使用这种格式,并像这样启动它:


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

1
我确定我不会有任何递归函数! - PermanentGuest

3
您可以使用一个有趣的技巧来避免大多数其他方法在尝试将库函数提供给主程序时出现的问题,而且它速度更快。使用此技巧的唯一要求是:
  • 库函数必须从主文件的代码块内部调用,
  • 在该代码块中不调用任何主文件函数。
这个技巧的关键在于“切换上下文”,使运行的批处理文件的上下文成为库文件;这样,库文件中的所有函数都可用于主代码块,无需额外处理。当然,在代码块结束前,运行的批处理文件的“上下文”必须切换回主文件。
“切换上下文”的方法是使用与运行主文件相同的名称重命名库文件(并将主文件重命名为另一个名称)。例如:
(
   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

请看我上面添加的示例。确保没有其他进程打开main.batlibrary.bat文件,例如文本编辑器。 - Aacini
这确实是一个有趣的方法,但我不会允许在我们部门中使用这种结构。一些原因包括: - Thomas
4
这确实是一个有趣的方法,但我不会允许在我们的部门中使用这种构造。一些原因是:代码不应该在用户可以更改的区域内,因此重命名肯定不是一个选项(特别是在多用户环境下)。这只是将巨大批处理文件的问题转移到另一个批处理文件中,如果您使用多个cmd库,这似乎毫无意义。由于我们正在谈论批处理文件,性能根本不是一个问题,在这里我真的看不出来。我认为这种方法可以在某些情况下有用,但不能作为一般方法。 - Thomas
1
不错的技巧,但我避免使用它,因为在从多个任务中使用时会产生一些问题,并且当出现错误时,重命名将不会被还原。但仍然很好.. +1 - jeb
@Thomas:关于“另一个巨大的批处理文件”和“多个cmd-libs”的评论,只要每个文件都遵循规则,你可以将这个机制分割成几个库文件。也许可以使用全局变量来控制这一点。我认为这种方法非常有趣,无论其是否有用...(感谢@jeb) - Aacini
显示剩余3条评论

2
我不确定原问题的上下文,但这可能是一种情况,可以切换到类似于VBScript或WPS的WSH,或者任何其他控制台脚本,而不是批处理文件。我将回答原始问题,但首先......需要一些背景和理解......
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 ...

这在库编写时考虑到这一点时,可以很好地工作,因此它会知道跳过第一个参数等等。
在Windows系统中使用更现代版本的CMD.EXE允许在CALL语法中使用":labels",如果您想限制参数范围(这使您可以使用%*表示“所有参数”,例如),则这可能非常有用,如下所示:
CALL :LABEL Parameter1 Paramater2 ...

(从同一批处理文件中或...)
CALL BATLIB.BAT :LABEL Parameter1 Parameter2 ...

关于这个的一些注释...在第一个形式中,:LABEL必须已经在当前批处理文件中。它将在CMD.EXE中创建一个新的“批处理上下文”,其中%*,%1,%2等与参数匹配。但您还必须提供某种返回/退出逻辑,以从该上下文返回/退出到调用上下文。
在第二种形式中,CMD.EXE并没有真正意识到您正在传递一个标签,因此您的批处理文件库将需要预期并处理它:
@ECHO OFF
CALL %*

这是因为命令解释器在尝试解析CALL命令之前就替换了%*,因此在变量扩展后,CALL命令会将:LABEL视为硬编码。这还会创建另一个批处理上下文的情况,因此您必须确保两次返回/退出该上下文:一次用于当前库上下文,再次返回到原始的CALL上下文。
仍然有其他方法可以实现批处理文件库,混合和匹配上述技术,或使用更复杂的逻辑,使用GOTO等等。这实际上是一个如此复杂的主题,以至于整本书都写了这个主题的专门章节,远比我在这里简单回答的要多得多!
到目前为止,我主要忽略了你将遇到的其他问题:如果CALL标签不存在怎么办?它将如何处理?环境变量扩展呢?它何时发生?如何防止它发生得太早?使用参数/参数中的特殊DOS字符怎么办?例如,解释器如何看待这样一行代码:CALL:ProcessPath %PATH%?(答案是CMD.EXE在处理CALL命令之前替换整个%PATH%。如果你的路径中有空格,这可能会引起问题,因为许多Windows的%PATH%变量都是这样的。例如C:\Program Files..)

正如你所看到的,事情很快就变得复杂和混乱了... 你必须停止像程序员那样思考,开始像COMMAND.COM/CMD.EXE那样思考,它几乎只看到一条单独的命令,而不是整个批处理文件作为一个原子单位。实际上,下面是一个示例,帮助你真正掌握它的工作方式...

创建一个名为C:\testing的文件夹,并将以下批处理文件命名为"oops.bat"放入其中:

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

现在打开一个控制台窗口并运行它,但让它停留在PAUSE处:
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!" 行的中间字节位置,因此它尝试在PAUSE之后将"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's "Hello, World,",接着是它自己的全名(%~dpnx0部分),CALLs临时批处理文件,该文件又ECHO's "Hi Mom!",接着是它自己的[随机]名称,然后返回到原始批处理文件,以便进行任何清理工作,例如在这种情况下删除临时批处理文件。

总之,正如您可以从本帖子的长度看出来的那样,这个主题确实不是一个简单的主题。网络上有数十个或更多网页提供大量批处理文件技巧、技巧等信息,其中许多深入探讨如何使用它们、创建批处理文件库、要注意哪些内容、如何按引用而非按值传递参数、如何管理变量何时何地被展开等等。

快速在谷歌上搜索“BATCH FILE PROGRAMMING”,你可以找到很多相关资源,你也可以查看维基百科和维基教科书、SS64.com、robvanderwoude.com,甚至是DMOZ的目录等网站获取更多资源。
祝好运!

1
这是一个CMD批处理脚本,可以将文件或文件夹(递归)导入到主脚本中:

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

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.
goto :eof

使用方法如下:
  • save it as import.cmd

  • call it with the /install flag in order to install it (does not require admin)

  • add a header like this at the begining of your main script that calls subroutines that are stored in other files - files that are going to be imported:

     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"
     )
    

要了解如何使用它,只需使用 /?标志调用它即可。


干得好!一些提示:
  1. 添加更多注释
  2. 减少冗余代码,例如8次 if /i "%~1" == "end"
  3. 使用 CALL import,否则它会中断调用者链 (test.bat将无法返回到调用者)
  4. 在块结束后取消 _import 的设置,否则在发生错误时会出现问题, 或者切换到 GOTO :afterImport 结构
  5. 避免仅因路径的添加而修改 AutoRun 设置,最好直接修改路径本身
  6. 找到一个解决方案,以便同时启动同一批处理两次
  7. 使用 call import end "%~f0"(或删除 %~0,因为没有帮助也可以检测到)
- jeb
@jeb:谢谢! - user11697062

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接