A batch file debugging library

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

A batch file debugging library

#1 Post by jfl » 16 Nov 2015 17:53

Hello,

I've been silently following this mailing list for quite some time, and I'd like to thank all participants for the great things I learned here.
Now it's time to contribute something back: First and foremost, I'd like to contribute my batch debugging library.
This library has allowed me to successfully debug and maintain very large system management scripts.
And the techniques published in this forum have allowed me to improve it a lot!
There's still a lot of room for further improvements though, and some of the new discoveries published here this year have the potential to lift many of its limitations and improve it much further.
Hopefully this can inspire some of you!

This library has a long history, and it actually has sister libraries for other scripting languages that I'm not going to detail here.
Over the years they have evolved around the following principles:
    - Scripts should have built-in debugging capabilities.
    - It should be easy to retrofit these debugging capabilities into existing scripts.
    - Scripts should work "normally" by default, and have standard options for enabling special debugging modes.
    - There should be no, or minimal performance impact when _not_ debugging a script.
    - The debug output should be valid batch code (Or their respective scripting languages for its sister libs).
    This is very imporatant, as this allows copying and pasting that code into another cmd window to reproduce problems.

Features:
    - Enforce structured programming techniques.
    - Defines macros %KEYWORDS% with names and functions that closely match the equivalent batch keyword. Ex: %ECHO% extends what echo does.
    - A reusable command-line parsing loop.
    - A verbose mode, enabled by the -v option, displaying more details than normal. This is to help users understand what is being done.
    - A debug mode, enabled by the -d option, displaying intermediate variables values, tracing code execution, etc. This is to help the script author fix bugs.
    - A no-execute mode, aka. WhatIf mode in PowerShell, enabled by the -X option, displaying all "dangerous" commands that should run, without actually running them. This is to help cautious people (both the author and the users) forecast catastrophes that may or may not happen if you run the script.
    - In debug mode, trace the execution of function calls and returned values; dump variables; etc.
    - Trace and conditionally execute external programs. (If not using the -X option for the NOEXEC mode)
    - Debug output indented by function call depth. (Very legible)

The library is downloadable at:
http://jf.larvoire.free.fr/progs/Library.bat
This file contains the debugging library, many useful routines (many of which were borrowed here!), and debug code for testing the debugger.

Library.bat structure:
    - A header comment block, with the file name, description, general notes, MIT license, and global change log.
    - A short library initialization: Defines a few important variables, and calls initialization routines for the core modules.
    - A macro definition module. (A new addition, now used my the optional %RETURN#% macro.)
    - The debug library core module. Begins by a header block describing it in details, with a change log. This module is required in all cases.
    - The execution tracing library module. Begins by a header describing it in details, with a change log. This module is optional.
    - A banner showing the end of the core debug library modules.
    - An eclectic list of useful routines collected over time. (Many of which coming from this forum.)
    - Routines for debugging the debugging library itself. (Yes, debuggers may have bugs too :-)
    - The header comment block for the main routine
    - The Help routine
    - The main routine, beginning by command-line arguments parsing.

I'm not going to describe everything in this post, but just one basic feature:
How to define a traceable function, and running it in normal and debug mode.
As an illustration, I'll use the factorial function defined in Library.bat, which calls itself recursively:

Code: Select all

:Fact
%FUNCTION% Fact %*
setlocal enableextensions enabledelayedexpansion
set N=%1
if .%1.==.. set N=0
if .%N%.==.0. (
  set RETVAL=1
) else (
  set /A M=N-1
  call :Fact !M!
  set /A RETVAL=N*RETVAL
)
endlocal & set "RETVAL=%RETVAL%" & %RETURN%


    * The function label must be followed by a line with the macro %FUNCTION%, followed by the function name, and %*.
    * The setlocal directive is optional, but recommended to allow defining local variables.
    * The last line uses the now standard (in this forum) technique for returning strings to the parent scope.
    * All exits from a %FUNCTION% MUST be done via a %RETURN%.
    * In debug mode, the value of the RETVAL variable will be displayed.

In normal mode, %FUNCTION% evaluates to rem, and %RETURN% evaluates to exit /b. The function runs at full speed, and returns the factorial.
Ex, using Library.bat self-testing capabilities:

Code: Select all

C:\JFL\SRC\Batch>Library.bat -c "call :Fact 5" "call :echovars RETVAL"
set "RETVAL=120"


In debug mode, %FUNCTION% evaluates to a "call :Debug.Entry", and %RETURN% evaluates to "call :Debug.Return & exit /b". These two calls manage the function tracing. Ex, again using Library.bat self-testing capabilities:

Code: Select all

C:\JFL\SRC\Batch>Library.bat -d -c "call :Fact 5" "call :echovars RETVAL"
call :Fact 5
  call :Fact 4
    call :Fact 3
      call :Fact 2
        call :Fact 1
          call :Fact 0
            return 1
          return 1
        return 2
      return 6
    return 24
  return 120
set "RETVAL=120"


Recently I've added a more sophisticated %RETURN#% macro, that can output any comment in debug mode, not just the RETVAL value like call :Debug.Return does.

Eventually I'd like to define a %RETURN_VARS% macro, that can pass multiple variables back to the parent scope, and display them all in debug mode.

To create new scripts with built-in debugging, the simplest is to copy the whole library.bat into a new script file, then...

    Update the name, description, notes, and change log in the file header.
    Remove the unneeded sections. Typically everything between the end banner of the debugging library, and the main routine header in the end.
    Remove the unnecessary options in the command-line processing loop in the main routine. (Those used for testing the library itself)
    Update the help screen.
    Replace the main routine content with your new code.
    Prefix all "dangerous" commands with the %EXEC% macro. This automates the no-exec operation.
    Add "%ECHOVARS.D% variable ..." lines for all tricky intermediate variables that change value.
    Use %FUNCTION% and %RETURN% or %RETURN#% directives for all your subroutines that need to be traced.
    All this is actually easier to do than it sounds. Writing the main routine has to be done anyway. Writing a good header and help screen is good practice. And the time lost in deleting the unneeded sections is negligible, compared to the time saved writing a command-line options processing loop.

To add the debugging library to old scripts

Here the need arises when an old script with no debugging options mysteriously fails. How to debug it, specially when you don't remember how it works, or worse still if it was written by somebody else, unreachable now.

The method is actually very similar to the case for new scripts:

    Insert a copy of the begugging library core sections immediately behind your script header comment.
    Add to your command-line processing loop the management for options -d, and possibly -v and -X from the library.
    Prefix all "dangerous" commands in the script with the %EXEC% macro. (Commands deleting files, writing data, etc.)
    Insert "%ECHOVARS.D% variable ..." lines for all tricky intermediate variables that change value.
    Add %FUNCTION% and %RETURN% or %RETURN#% directives for all your subroutines that need to be traced.
    Finally run the modified script with the -d option, to see what happens internally before the problem occurs.

Enjoy!

Jean-François

Ed Dyreen
Expert
Posts: 1569
Joined: 16 May 2011 08:21
Location: Flanders(Belgium)
Contact:

Re: A batch file debugging library

#2 Post by Ed Dyreen » 16 Nov 2015 22:51

Interesting, so basically you've been doing the same thing I have been doing, building libraries of functions.
jfl wrote:- Defines macros %KEYWORDS% with names and functions that closely match the equivalent batch keyword. Ex: %ECHO% extends what echo does.
Macro's have a dramatically and linearly negative effect on the performance of setlocal etc.. How about a generic function

Code: Select all

( %help% command )
or the standard argument way

Code: Select all

( %command% /? )
jfl wrote:I'm not going to describe everything in this post, but just one basic feature:
How to define a traceable function, and running it in normal and debug mode.
As an illustration, I'll use the factorial function defined in Library.bat, which calls itself recursively:

Code: Select all

:Fact
%FUNCTION% Fact %*
setlocal enableextensions enabledelayedexpansion
set N=%1
if .%1.==.. set N=0
if .%N%.==.0. (
  set RETVAL=1
) else (
  set /A M=N-1
  call :Fact !M!
  set /A RETVAL=N*RETVAL
)
endlocal & set "RETVAL=%RETVAL%" & %RETURN%
Again I'd use a generic function handler instead of having the function do the work, something like.

Code: Select all

:§parser_ ( "func", "args","" )
   ( %setLocal_% "§%~1" )
   call :!$trace! !*! ^!
   ( %endlocal_% optionalNumberOfSetlocals, $r, !$r! )
exit /B %£e%
and then have this function handle the call stack arguments, that can later be retrieved by errorHandler etc...

Code: Select all

set/A$debug+=1,£e=0
set $trace=%%~?
set $debug[^^^!$debug^^^!]=%%~?
set %%~?.p=^^^!*^^^! 
jeb introduced another way by calculating which line is causing the error but this won't work for macro's.
jfl wrote:* In debug mode, the value of the RETVAL variable will be displayed.
or a return value that contains all variable names that need returning.

Code: Select all

set "this var=data"
set $r=thisVar, thatVar, "this var"
jfl wrote:In normal mode, %FUNCTION% evaluates to rem, and %RETURN% evaluates to exit /b. The function runs at full speed, and returns the factorial.
Not if you are loading a set of variables in memory of course.
jfl wrote:In debug mode, %FUNCTION% evaluates to a "call :Debug.Entry", and %RETURN% evaluates to "call :Debug.Return & exit /b". These two calls manage the function tracing. Ex, again using Library.bat self-testing capabilities:
Here I'd use a macro due to the repetitive characteristics
jfl wrote:Eventually I'd like to define a %RETURN_VARS% macro, that can pass multiple variables back to the parent scope, and display them all in debug mode.
I have that macro, I can port it to a standalone macro ready to be loaded in delayed or disdelayed. but it only works on winXP, win7 and above can cause unpredictable behavior but easily tested. If I post the code you'll be baffled because it is actually a memory dump of the macro in question, which source-code I am willing to share to some extend, you won't be able to use the source-code unless I provide the entire library and that might raise more questions than answers ( I don't come here too often lately since I don't default to batch any longer ). Dave benham's functions typically echo the result if a returnVariable is not provided, but I can't use that as I usually define the returnVariable as required. So I have %endlocal_% echo the return but only if debugMode is set.

Code: Select all

setlocal
set varA=foo
set varB=bar
( %endlocal_% varA, varB )
You may wonder why I use braces around macro's, this is to make sure the batch will crash if %endlocal_% is missing or corrupt.

jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

Re: A batch file debugging library

#3 Post by jfl » 17 Nov 2015 11:58

Macro's have a dramatically and linearly negative effect on the performance of setlocal etc..

Performance is excellent in all my scripts, with no noticeable slowdown in non-debug mode. (I'm not doing math, I'm doing system configuration. So the script is rarely the performance limitation, it's the programs it runs that are. But note that early versions of my library _did_ have noticeably bad performance effects. It's like Rolls Royce cars now: The engine power does not matter: It is sufficient as it is. :) )
As for the performance of this particular factorial function, this is not the point. I know it could be done far more efficiently. I only defined it as a test/demo of the tracing capabilities, showing the indentation of the output.

How about a generic function

This is precisely the point: I'm trying to make the library as simple to use as possible by staying as close as possible to the syntax of the language. Here, I'm using %ECHO% as a better replacement for echo to output an argument line.
- In debug mode, it outputs it indented at the function level. This makes the debug output considerably easier to read.
- In all cases, if a log file is defined, it will also output that line into the log file.
But I must not change the structure of the echo command line, else I'll get lost in my own scripts! (I'm working with scripts in 4 different scripting languages.)

Again I'd use a generic function handler instead of having the function do the work, something like.

I agree this would be more powerful, but again here the goal is to make the usage as simple as possible.
Inserting a %FUNCTION% macro in the beginning of a function and a %RETURN% macro in the end is the least intrusive I could find.
Actually since yesterday, I realized that requiring to specify the function name and %* behind the %FUNCTION% macro is useless: This can be automatically generated by the macro itself by doing call call ... %%0 %%*. I'll remove this requirement in the next library update when I have time.

Also I noticed the major discoveries here in June allowing to output a call stack with error messages.
I've not felt the need for that so far though, because the way my library presents calls indented, the call stack is obviously visible by a simple glance at the trace log.
Of course, there are major advantages of the forum's call stack discovery, as it works even on non-instrumented functions, even in non-debug mode, etc. But note that my function tracing also works across multiple scripts, indenting the output correctly in this case too.

I have that macro, I can port it to a standalone macro ready to be loaded in delayed or disdelayed. but it only works on winXP, win7 and above can cause unpredictable behavior but easily tested.

Thanks for the proposition, but this needs to work in all versions of Windows.
I'm sure this can relatively easily be done by adapting Dave Benham's code in this post: http://www.dostips.com/forum/viewtopic.php?p=41929#p41929
Again, I'll do it when I have time.

Jean-François

Ed Dyreen
Expert
Posts: 1569
Joined: 16 May 2011 08:21
Location: Flanders(Belgium)
Contact:

Re: A batch file debugging library

#4 Post by Ed Dyreen » 17 Nov 2015 14:55

I do it a little different. I call the library which will load my functions and then returns to the caller. Once the program is done I sometimes tell the library to build a standalone batch. The resulting batch will only load memory dumps of the functions that are actually used. A side effect is that the batches do not include the source code and the inability for the end user to debug or alter the build ( memory dumps are extremely hard for the human to understand ). This is for example a few lines of a disDelayed endlocal_ dump;

cache\macro\notDelayed\endlocal_.CMD

Code: Select all

:: (endlocal_:disDelayed) '1825' bytes on file, '1240' bytes in memory.
:: (
%=   =%for %%# in ("") do set ^"endlocal_=for %%? in (1,2) do if %%?==2 (for /F ^"usebacktokens=1-4eol=¦delims=¦^" %%i in ('%%^!^"^"^!1¦%%^!^"^"^!2¦%%^!^"^"^!3¦%%^!^"^"^!~4') do for /F ^"usebacktokens=1-2eol=¦delims=¦^" %%r in ('^!pathext^!¦^!path^!') do (set pathext=;^&set path=%$n1c%
%=      =%set ¤=^&for %%? in (^!º^!) do if ^!¤^!.==. set/A¤=%%~?0/10^&if ^!¤^! NEQ 0 (call set º=%%º:*^!¤^!=%%^&set/A¤+=1)else set/A¤=2%$n1c%
%=      =%set _=^&for %%? in (^!º^!) do (set ?=^!%%~?^!^&if defined ? (set ?=^!?:%%=%%i^!%$n1c%
%=      =%for /F %%r in (^"^!$cr^! ^") do set ?=^!?:%%r=%%k^!%$n1c%
%=      =%set ?=^!?:^^^%$lf%%$lf%^%$lf%%$lf%=%%l^!%$n1c%
%=      =%set ?=^!?:%%=%%i^!)%$n1c%
%=      =%set _=^!_^!%%l%%~?#=^!?^!)%$n1c%
%=      =%set _=^!_:^"=%%j^!%$n1c%
%=      =%set ^"_=^!_:^^=%%9^!^"^&call set ^"_=%%^^_:^^^!=^"^"%%^"%$n1c%
%=      =%for /F delims^^= %%r in (^"^!_:~3^!^") do (for /L %%i in (1,1,^!¤^!) do endlocal%$n1c%
%=      =%set ?=^!^&setlocal enableDelayedExpansion%$n1c%
%=      =%set _=%%r%$n1c%
etcetera..
Simplicity and performance don't go well together, obfuscation is the result of the performance optimization process, the memory dump and the result a black-box function which cannot be maintained. Any changes in the source function trigger a rebuild of the cache, the memory dump.

So this is the source code of the endlocal_ macro, the code that I manually wrote, above is the cache partially, the memory dump that returns multiple variables, the function you want to build. You can use dave's functions too ofcourse, a function is easier than a macro and has con's and pro's.

Code: Select all

::-------------------------------------------------------------------------------------------------------------------------------------
( %macroStart_% endlocal_, enableDelayedExpansion )
::-------------------------------------------------------------------------------------------------------------------------------------
:: last updated       : 2015^06^01
:: support            : naDelayed, [ $cr, $lf ]
::
:: maximum return size = 8k which is the maximum of the sum of all memory sizes of all variables provided
:: overflow returns void and silent !
:: NOT designed for large values, use endlocalBig_ instead
::
set ^"$usage.%$macro%=^
%=       =% use: ( %%endlocal_%% "eCount", "var", "etc.." )%$n1c%
%=       =% err: Unaffected, panic otherwise"
:: (
%=       =%for %%# in ("") do set ^"%$macro%=!forQ_! (1,2) do if %%?==2 (%$c1%
%$c1%
%=              =%for /F "usebacktokens=1-4eol=¦delims=¦" %%i in (%=       protect tokens from external percent expansion   =%%$c1%
%=                            =%'%%^^^!""^^^!1¦%%^^^!""^^^!2¦%%^^^!""^^^!3¦%%^^^!""^^^!~4'%=           function.endlocalRF_ =%%$c1%
%=              =%) do for /F "usebacktokens=1-2eol=¦delims=¦" %%r in ('^^^!pathext^^^!¦^^^!path^^^!') do (%=callSetVarSafe =%%$c1%
%=                     =%set pathext=;^&set path=%$n1c%
%=                     =%set ¤=^&!forQ_! (^^^!º^^^!) do if ^^^!¤^^^!.==. set/A¤=%%~?0/10^&if ^^^!¤^^^! NEQ 0 (%$c1%
%=                            =%call set º=%%º:*^^^!¤^^^!=%%^&set/A¤+=1%=                              endlocal count       =%%$c1%
%=                     =%)else set/A¤=2%$n1c%
%=                     =%set _=^&!forQ_! (^^^!º^^^!) do (%$c1%
%=                            =%set ?=^^^!%%~?^^^!^&if defined ? (%$c1%
%=                                   =%set ?=^^^!?:%%=%%i^^^!%=                                        coded mark set       =%%$n1c%
%=                                   =%for /F %%r in ("^!$cr^! ") do set ?=^^^!?:%%r=%%k^^^!%=         enco $cr             =%%$n1c%
%=                                   =%set ?=^^^!?:^^^^!$lf!!$lf!=%%l^^^!%=                            enco $lf             =%%$n1c%
%=                                   =%set ?=^^^!?:%%=%%i^^^!%=                                        coded mark set       =%%$c1%
%=                            =%)%$n1c%
%=                            =%set _=^^^!_^^^!%%l%%~?#=^^^!?^^^!%=                                    coded add $lf        =%%$c1%
%=                     =%)%$n1c%
%=                     =%set _=^^^!_:^^^"=%%j^^^!%=                                                    code pre 94, 33      =%%$n1c%
%=                     =%set "_=^!_:^^^=%%9^!"^&call set "_=%%^^^_:^^^!=""%%"%=                        enco     94, 33      =%%$n1c%
%=                     =%for /F delims^^^^= %%r in ("^!_:~3^!") do (%$c1%
%=                            =%!forI_! (1,1,^^^!¤^^^!) do endlocal%$n1c%
%=                            =%set ?=^^^!^&setlocal enableDelayedExpansion%=                          outer delay          =%%$n1c%
%=                            =%set _=%%r%$n1c%
%=                            =%call set "_=%%^^^_:""=""^^^!%%"^&if ^^^!?^^^!.==. (%=                  delay 2-times        =%%$c1%
%=                                   =%set "_=^!_:%%9=^^^^^!"^&set "_=^!_:""=^^^!"%=                   deco     94, 33      =%%$n1c%
%=                                   =%set _=^^^!_:%%j=%e7%"^^^!%=                                     code pst 94, 33      =%%$c1%
%=                            =%)else set "_=^!_:%%9=^^^!"^&set _=^^^!_:""=^^^!%$n1c%
%=                            =%!for@_! ("^!$lf^!") do set _=^^^!_:%%l=%%~@^^^!%=                      deco $lf             =%%$n1c%
%=                            =%set _=^^^!_:%%i=%%^^^!%=                                               coded mark unset     =%%$n1c%
%=                            =%!forQ_! ("^!$function.fullPathFile^!") do for /F "tokens=1,*eol=#delims=#" %%r in ("^!_^!") do (%$c1%
%=                                   =%endlocal%$n1c%
%=                                   =%if ^^^!.==. (%=                                                 return.delayed       =%%$c1%
%=                                          =%set _=%%s^^^!%$n1c%
%=                                          =%if defined _ (%$c1%
%=                                                 =%set _=^^^!_:%%l=^^^^!$lf!!$lf!^^^!%=              deco $lf             =%%$n1c%
%=                                                 =%for /F %%r in ("^!$cr^! ") do set _=^^^!_:%%k=%%r^^^!%=deco $cr        =%%$n1c%
%=                                                 =%set _=^^^!_:%%i=%%^^^!%=                          coded mark unset     =%%$c1%
%=                                          =%)%$n1c%
%=                                          =%set "%%r=^!_:~1^!"^^^!%$c1%
%=                                   =%)else (%=                                                       return.notDelayed    =%%$c1%
%=                                          =%set a=%%r^&set b%%s^&set c=%$n1c%
%=                                          =%call %%? §return.notDelayed%$c1%
%=                                   =%)%$n1c%
%=                                   =%setlocal enableDelayedExpansion%$c1%
%=                            =%)%$n1c%
%=                            =%endlocal%$c1%
%=                     =%)%$n1c%
%=                     =%set pathext=%%r^&set path=%%s%$c1%
%=              =%)%$c1%
%$c1%
%=       =%)else setlocal enableDelayedExpansion^&set º="
:: )
%macroEnd% 2>nul
%endlocalR_% (
%=       =%%$0%
%=       =%%$1%
)
So something like that or a variation on dave's return.

Consider well which functions you want as static variables or macro's and which you want to keep as functions. Any memory occupied will impact global performance negatively even if you unload the memory you won't regain performance and can be considered lost. If you convert a function to a macro and then only access it once or twice during the run it won't pay off. endlocal_ macro is used so often that it beats the crap out of endlocal_ function performance wise. Macros are chosen for often used tasks and functions are chosen for large, complex or rarely used tasks.

ps; try to avoid the use of call set and if you have to unset %path% and %pathext% beforehand, call set is notoriously slow.


Good luck.

jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

Re: A batch file debugging library

#5 Post by jfl » 28 Nov 2015 11:17

It is strange how posting something on a forum makes you think harder about it!

I finally followed your advice, and rewrote my macros to avoid calling subroutines.
But this proved more difficult than I thought, and there are still corner cases where I did not manage to avoid it. More on this later.
Still, the new code is indeed more powerful AND faster than the old one in most cases. Thanks for insisting. :)

Then I went ahead and added the ability to return any number of variables, and log them and display them all in debug mode.
As ease of use is my first priority, I had to find a simple syntax for declaring what variables to return... A syntax that could also be implemented with batch macros.
After trying a few unsatisfying alternatives, I got inspiration from the Tcl language, and took its upvar keyword. It becomes a new %UPVAR% macro here.
This also required a major change to my %FUNCTION% and %RETURN% macros, but a change that actually makes them better and simpler to use :D :

%FUNCTION% is now semantically equivalent to setlocal. It accepts the same arguments as setlocal: [EnableDelayedExpansion|DisableDelayedExpansion]

%UPVAR% grows a list of variable names, that will be passed back to the caller upon return.
Like Tcl's upvar, it can be invoked multiple times to declare multiple variables.
Unlike Tcl's upvar, you can't define a different name for the local proxy and parent variables. But you can define several names at the same time.

%RETURN% is now semantically equivalent to endlocal & exit /b. Its only argument is an optional exit code.

In the new Library.bat version now posted at http://jf.larvoire.free.fr/progs/Library.bat, the sample factorial routine becomes:

Code: Select all

:Fact
%FUNCTION% enableextensions enabledelayedexpansion
%UPVAR% RETVAL
set N=%1
if .%1.==.. set N=0
if .%N%.==.0. (
  set RETVAL=1
) else (
  set /A M=N-1
  call :Fact !M!
  set /A RETVAL=N*RETVAL
)
%RETURN%

Notice how the return code is now more intuitive, and does not require the user to understand the subtleties of the batch language :? endlocal bypass techniques!

When invoked in test mode, the output is exactly the same as before:

Code: Select all

2 C:\JFL\SRC\Batch>Library.bat -d -c "call :Fact 5" "call :echovars RETVAL"
CMD[1]=call :Fact 5
CMD[2]=call :echovars RETVAL
call :Fact 5
  call :Fact 4
    call :Fact 3
      call :Fact 2
        call :Fact 1
          call :Fact 0
            return 0 & set "RETVAL=1"
          return 0 & set "RETVAL=1"
        return 0 & set "RETVAL=2"
      return 0 & set "RETVAL=6"
    return 0 & set "RETVAL=24"
  return 0 & set "RETVAL=120"
set "RETVAL=120"

2 C:\JFL\SRC\Batch>

Internally, the %RETURN% macro work somewhat like in Dave Benham's return.bat.
I had to make changes, because it has to deal with a varying number of return variables, possibly zero.
So it cannot predefine two output values, for the two parent expansion possibilities.
Instead, I chose to preprocess the list of variables to return, and do the final expansion after returning to the parent variable scope.
This relies on a set of predefined entity variables, that will expand to the correct character at the last stage:
(The character entity names were borrowed from HTML entities, or from ASCII control codes names.)
(In my library they all have a "DEBUG." namespace prefix to their name, but I'm not showing it here for the sake of clarity.)

Code: Select all

set "percnt=%%" &:# One percent sign
set "excl=^!"   &:# One exclamation mark
set "hat=^"     &:# One circumflex accent, aka. hat sign
set ^"quot=""   &:# One double quote
set "amp=&"     &:# One ampersand
set "vert=|"    &:# One vertical bar
set "gt=>"      &:# One greater than sign
set "lt=<"      &:# One less than sign
set "sp= "      &:# One space
...

The list of variables, for ex: "VAR1 VAR2", with VAR1==123 and VAR2=="^ !"
is preprocessed to an intermediate list: "VAR1=123" "VAR2=%quot%%hat%%sp%%excl%%quot%"
Then this intermediate list is processed a first time in debug mode to display the return trace. Ex here:

Code: Select all

  return 0 & set "VAR1=123" & set "VAR2=="^ !""

Note that the order this is shown reflects the reality that the return is done first, THEN the variables are set.
Then the intermediate list is processed a second time when back at the parent variable scope:
If delayed expansion is enabled, it is first converted to: "VAR1=123" "VAR2=!quot!!hat!!sp!!excl!!quot!"
Finally a loop assigns each variable in turn: (set "VAR1=123"), (set "VAR2=!quot!!hat!!sp!!excl!!quot!")

Adding support for returning the CR and LF characters was as simple as adding two entity definitions.

One last goodie with this scheme: I've updated the -c, -b, -a test options to recognize entities within brackets, and change them to the corresponding character before running the test command.
This allows verifying that the new %FUNCTION%, %UPVAR%, and %RETURN% macros work by running commands such as these:

Code: Select all

2 C:\JFL\SRC\Batch>Library.bat -d -c "setlocal DisableDelayedExpansion" "call :noop44 2 [quot]a[hat][hat]b[hat][excl]c[percnt][percnt][percnt][percnt]d[quot] {[quot]) ([quot]}" "set RETVAL1" "set RETVAL2" "endlocal"
CMD[1]=setlocal DisableDelayedExpansion
CMD[2]=call :noop44 2 "a^^b^!c%%%%d" {") ("}
CMD[3]=set RETVAL1
CMD[4]=set RETVAL2
CMD[5]=endlocal
call :noop44 2 "a^^b^!c%%d" {") ("}
  call :noop4 2 "a^^b^!c%d" {") ("}
    return 2 & set "RETVAL1="a^^b^!c%d"" & set "RETVAL2={") ("}"
  return 2 & set "RETVAL1="a^^b^!c%d"" & set "RETVAL2={") ("}"
RETVAL1="a^^b^!c%d"
RETVAL2={") ("}

2 C:\JFL\SRC\Batch>Library.bat -c "setlocal DisableDelayedExpansion" "call :noop44 2 [quot]a[hat][hat]b[hat][excl]c[percnt][percnt][percnt][percnt]d[quot] {[quot]) ([quot]}" "set RETVAL1" "set RETVAL2" "endlocal"
RETVAL1="a^^b^!c%d"
RETVAL2={") ("}

2 C:\JFL\SRC\Batch>Library.bat -d -c "setlocal EnableDelayedExpansion" "call :noop44 2 [quot]a[hat][hat]b[hat][excl]c[percnt][percnt][percnt][percnt]d[quot] {[quot]) ([quot]}" "set RETVAL1" "set RETVAL2" "endlocal"
CMD[1]=setlocal EnableDelayedExpansion
CMD[2]=call :noop44 2 "a^^b^!c%%%%d" {") ("}
CMD[3]=set RETVAL1
CMD[4]=set RETVAL2
CMD[5]=endlocal
call :noop44 2 "a^^b^!c%%d" {") ("}
  call :noop4 2 "a^^b^!c%d" {") ("}
    return 2 & set "RETVAL1="a^b!c%d"" & set "RETVAL2={") ("}"
  return 2 & set "RETVAL1="a^b!c%d"" & set "RETVAL2={") ("}"
RETVAL1="a^b!c%d"
RETVAL2={") ("}

2 C:\JFL\SRC\Batch>Library.bat -c "setlocal EnableDelayedExpansion" "call :noop44 2 [quot]a[hat][hat]b[hat][excl]c[percnt][percnt][percnt][percnt]d[quot] {[quot]) ([quot]}" "set RETVAL1" "set RETVAL2" "endlocal"
RETVAL1="a^b!c%d"
RETVAL2={") ("}

2 C:\JFL\SRC\Batch>


Here's the %FUNCTION%, %UPVAR%, and %RETURN% macros source:

Code: Select all

set FUNCTION=%MACRO% ( %\n%
  call set "FUNCTION.NAME=%%0" %\n%
  call set "ARGS=%%*"%\n%
  setlocal EnableDelayedExpansion %# Avoid eating up hats and excl in output #% %\n%
  if defined ^^%>%DEBUGOUT ( %# See the TO DO note at the end of the RETURN macro #% %\n%
    call :Echo.2DebugOut call %!%FUNCTION.NAME%!% %!%ARGS%!%%\n%
  ) else ( %\n%
    echo%!%INDENT%!% call %!%FUNCTION.NAME%!% %!%ARGS%!%%\n%
  ) %\n%
  if defined LOGFILE ( %\n%
    call :Echo.2LogFile call %!%FUNCTION.NAME%!% %!%ARGS%!%%\n%
  ) %\n%
  endlocal %\n%
  call set "INDENT=%%INDENT%%  " %\n%
  set "DEBUG.RETVARS=" %\n%
) else setlocal &:# No need for %/MACRO% here, as the only macro arguments are those passed to setlocal

set UPVAR=call set DEBUG.RETVARS=%%DEBUG.RETVARS%%

set RETURN=call set "DEBUG.ERRORLEVEL=%%ERRORLEVEL%%" %&% %MACRO% ( %\n%
  set DEBUG.EXITCODE=%!%MACRO.ARGS%!%%\n%
  if defined DEBUG.EXITCODE set DEBUG.EXITCODE=%!%DEBUG.EXITCODE: =%!%%\n%
  if not defined DEBUG.EXITCODE set DEBUG.EXITCODE=%!%DEBUG.ERRORLEVEL%!%%\n%
  set "DEBUG.SETARGS=" %\n%
  for %%v in (%!%DEBUG.RETVARS%!%) do ( %\n%
    set "DEBUG.VALUE=%'!%%%v%'!%" %# We must remove problematic characters in that value #% %\n%
    set "DEBUG.VALUE=%'!%DEBUG.VALUE:%%=%%DEBUG.percnt%%%'!%"   %# Remove percent #% %\n%
    for %%e in (sp tab cr lf quot) do for %%c in ("%'!%DEBUG.%%e%'!%") do ( %# Remove named character entities #% %\n%
      set "DEBUG.VALUE=%'!%DEBUG.VALUE:%%~c=%%DEBUG.%%e%%%'!%" %\n%
    ) %\n%
    set "DEBUG.VALUE=%'!%DEBUG.VALUE:^^=%%DEBUG.hat%%%'!%"      %# Remove circumflex accent #% %\n%
    call set "DEBUG.VALUE=%%DEBUG.VALUE:%!%=^^^^%%"             %# Remove exclamation points #% %\n%
    set "DEBUG.VALUE=%'!%DEBUG.VALUE:^^^^=%%DEBUG.excl%%%'!%"   %# Remove exclamation points #% %\n%
    set DEBUG.SETARGS=%!%DEBUG.SETARGS%!% "%%v=%'!%DEBUG.VALUE%'!%" %\n%
  ) %\n%
  if %!%DEBUG%!%==1 ( %\n%
    set "DEBUG.MSG=return %'!%DEBUG.EXITCODE%'!%" %\n%
    for %%v in (%!%DEBUG.SETARGS%!%) do ( %\n%
      set "DEBUG.MSG=%'!%DEBUG.MSG%'!% %%DEBUG.AND%% set %%v" %!% %\n%
    ) %\n%
    setlocal DisableDelayedExpansion %\n%
    set "DEBUG.excl=^^%'!%" %\n%
    set "DEBUG.hat=^^^^" %\n%
    set "DEBUG.AND=^^^&" %\n%
    if defined ^^%>%DEBUGOUT ( %# See the TO DO note at the end of this macro #% %\n%
      call :Echo.Eval2DebugOut %%DEBUG.MSG%%%\n%
    ) else ( %\n%
      call call echo %%INDENT%%%%DEBUG.MSG%%%\n% %# Convert entities back to chars #% %\n%
    ) %\n%
    if defined LOGFILE ( %\n%
      call :Echo.Eval2LogFile %%DEBUG.MSG%%%\n%
    ) %\n%
    endlocal %\n%
  ) %\n%
  for %%r in (%!%DEBUG.EXITCODE%!%) do ( %\n%
    for /f "delims=" %%a in (""" %'!%DEBUG.SETARGS%'!%") do ( %# "" makes sure the body runs even if the list is empty #% %\n%
      endlocal %&% endlocal %&% if 1==1 ( %# Exit the RETURN and FUNCTION locals #% %\n%
        if "%'!%%'!%"=="" ( %# Delayed expansion is ON #% %\n%
          set "DEBUG.SETARGS=%%a" %\n%
          call set "DEBUG.SETARGS=%'!%DEBUG.SETARGS:%%=%%DEBUG.excl%%%'!%" %# Change all percent to ! #%  %\n%
          for %%v in (%!%DEBUG.SETARGS:~3%!%) do ( %\n%
            set %%v %\n%
          ) %\n%
        ) else ( %# Delayed expansion is OFF #% %\n%
          set "DEBUG.excl=%'!%" %\n%
          set "DEBUG.hat=^^^^" %\n%
          for %%v in (%%a) do if not %%v=="" ( %\n%
            call set %%v %\n%
          ) %\n%
        ) %\n%
      ) %\n%
      exit /b %%r %\n%
    ) %# Exit the RETURN and FUNCTION setlocals #% %\n%
  ) %\n%
) %/MACRO%

The corner cases I was talking about are related to the optional logging and debug output stream.
I've not managed to find a way to expand variables with > standard output redirections within the %RETURN% macro.
(Actually the expansion works, and %RETURN% appears to include syntactically valid redirections, but they do not work.)
This is why I'm testing for the exsitence of variables ">DEBUGOUT" and "LOGFILE", and calling small helper routines if they do.
Any clue as to how to fix this will help making these macros leaner and faster.

One last improvement idea that I'll try to do is to dynamically change the %RETURN% macro:
In non-debug mode, %FUNCTION% could redefine %RETURN% as "endlocal & exit /b".
Then %UPVAR% could change it back to the full-fledged version.

jfl
Posts: 226
Joined: 26 Oct 2012 06:40
Location: Saint Hilaire du Touvet, France
Contact:

Re: A batch file debugging library

#6 Post by jfl » 30 Nov 2015 09:20

I understood at last why my delayed redirection code did not work:
This is a well known limitation (at least to old timers on this forum) of the cmd parser, and I was trying to do the impossible. :cry:
As explained in http://www.dostips.com/forum/viewtopic.php?f=3&t=6587#p42330, in a call command, redirections are parsed but completly ignored; And the delayed expansion and for-loop-parameter expansion phases are not done at all. More bizarre limitations to be aware of!
So using outside helper functions is unavoidable when a macro needs to redirect output to a variable file.

Anyway, while experimenting with this, I found how to overcome the call's delayed expansion limitation:
(But unfortunately not the for-loop-parameter one)
Instead of using call to try doing a double !variable! evaluation, it is actually possible to do it precisely by using a for loop:

Code: Select all

for /f "delims=" %%s in ("!variable!") do set "result=%%s"

After the %s loop variable is evaluated, the !subvariables! within it are evaluated again.
This applies to my %RETURN% macro, allowing to simplify it a bit, and even fixing some display issues when the final value contained CR or LF characters.
(In the previous %RETURN% version, the value vas returned correctly, but displayed truncated in the debug output.)

After this, and a few other simplifications, the new Library.bat version now contains the %FUNCTION%, %UPVAR% and %RETURN% macros as in this smaller test program:
(Contrary to my previous posts, this one is fully functional, and does not require downloading the whole Library.bat)

[2015-12-02 edit: Fixed a bug in the %FUNCTION% macro, which disabled expansion when invoked with no argument]

Code: Select all

@echo off
setlocal EnableExtensions EnableDelayedExpansion

call :Macro.Init
call :Debug.Init
goto :main

:#----------------------------------------------------------------------------#

:Macro.Init
:# Define a LF variable containing a Line Feed ('\x0A')
:# The two blank lines below are necessary.
set LF=^


:# End of define Line Feed. The two blank lines above are necessary.

:# LF generator variables, that become an LF after N expansions
:# %LF1% == %LF% ; %LF2% == To expand twice ; %LF3% == To expand 3 times ; Etc
:# Starting with LF2, the right # of ^ doubles on every line,
:# and the left # of ^ is 3 times the right # of ^.
set ^"LF1=^%LF%%LF%"
set ^"LF2=^^^%LF1%%LF1%^%LF1%%LF1%"
set ^"LF3=^^^^^^%LF2%%LF2%^^%LF2%%LF2%"
set ^"LF4=^^^^^^^^^^^^%LF3%%LF3%^^^^%LF3%%LF3%"
set ^"LF5=^^^^^^^^^^^^^^^^^^^^^^^^%LF4%%LF4%^^^^^^^^%LF4%%LF4%"

:# Variables for use in inline macro functions
set ^"\n=%LF3%^^^"      &:# Insert a LF and continue macro on next line
set "^!=^^^^^^^!"       &:# Define a %!%DelayedExpansion%!% variable
set "'^!=^^^!"          &:# Idem, but inside a quoted string
set ">=^^^>"            &:# Insert a redirection character
set "&=^^^&"            &:# Insert a command separator in a macro
:# Idem, to be expanded twice, for use in macros within macros
set "^!2=^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^!"
set "'^!2=^^^^^^^!"
set "&2=^^^^^^^^^^^^^^^&"

set "MACRO=for %%$ in (1 2) do if %%$==2"                               &:# Prolog code of a macro
set "/MACRO=else setlocal enableDelayedExpansion %&% set MACRO.ARGS="   &:# Epilog code of a macro

set "ON_MACRO_EXIT=for /f "delims=" %%r in ('echo"      &:# Begin the return variables definitions
set "/ON_MACRO_EXIT=') do endlocal %&% %%r"             &:# End the return variables definitions

:# While at it, and although this is unrelated to macros, define other useful ASCII control codes
:# Define a CR variable containing a Carriage Return ('\x0D')
for /f %%a in ('copy /Z %COMSPEC% nul') do set "CR=%%a"

:# Define a BS variable containing a BackSpace ('\x08')
:# Use prompt to store a  backspace+space+backspace into a DEL variable.
for /F "tokens=1 delims=#" %%a in ('"prompt #$H# & echo on & for %%b in (1) do rem"') do set "DEL=%%a"
:# Then extract the first backspace
set "BS=%DEL:~0,1%"

goto :eof

:#----------------------------------------------------------------------------#

:Debug.Init

set "ECHO=call :Echo"

:# Define variables for problematic characters, that cause parsing issues
:# Use the ASCII control character name, or the html entity name.
:# Warning: The excl and hat characters need different quoting depending on context.
set "DEBUG.percnt=%%"   &:# One percent sign
set "DEBUG.excl=^!"     &:# One exclamation mark
set "DEBUG.hat=^"       &:# One caret, aka. circumflex accent, or hat sign
set ^"DEBUG.quot=""     &:# One double quote
set "DEBUG.apos='"      &:# One apostrophe
set "DEBUG.amp=&"       &:# One ampersand
set "DEBUG.vert=|"      &:# One vertical bar
set "DEBUG.gt=>"        &:# One greater than sign
set "DEBUG.lt=<"        &:# One less than sign
set "DEBUG.lpar=("      &:# One left parenthesis
set "DEBUG.rpar=)"      &:# One right parenthesis
set "DEBUG.lbrack=["    &:# One left bracket
set "DEBUG.rbrack=]"    &:# One right bracket
set "DEBUG.sp= "        &:# One space
set "DEBUG.tab= "       &:# One tabulation
set "DEBUG.cr=!CR!"     &:# One carrier return
set "DEBUG.lf=!LF!"     &:# One line feed
set "DEBUG.bs=!BS!"     &:# One backspace

:# The FUNCTION, UPVAR, and RETURN macros should work with delayed expansion on or off
set MACRO.GETEXP=(if "%'!2%%'!2%"=="" (set MACRO.EXP=EnableDelayedExpansion) else set MACRO.EXP=DisableDelayedExpansion)

set FUNCTION=%MACRO.GETEXP% %&% %MACRO% ( %\n%
  call set "FUNCTION.NAME=%%0" %\n%
  call set "ARGS=%%*"%\n%
  if %!%DEBUG%!%==1 ( %# Build the debug message and display it #% %\n%
    if defined ^^%>%DEBUGOUT ( %# If we use a debugging stream distinct from stdout #% %\n%
      call :Echo.2DebugOut call %!%FUNCTION.NAME%!% %!%ARGS%!%%# Use a helper routine, as delayed redirection does not work #%%\n%
    ) else ( %# Output directly here, which is faster #% %\n%
      echo%!%INDENT%!% call %!%FUNCTION.NAME%!% %!%ARGS%!%%\n%
    ) %\n%
    if defined LOGFILE ( %# If we have to send a copy to a log file #% %\n%
      call :Echo.2LogFile call %!%FUNCTION.NAME%!% %!%ARGS%!%%# Use a helper routine, as delayed redirection does not work #%%\n%
    ) %\n%
    call set "INDENT=%'!%INDENT%'!%  " %\n%
  ) %\n%
  set "DEBUG.RETVARS=" %\n%
  if not defined MACRO.ARGS set "MACRO.ARGS=%'!%MACRO.EXP%'!%" %\n%
  setlocal %!%MACRO.ARGS%!% %\n%
) %/MACRO%

set UPVAR=call set DEBUG.RETVARS=%%DEBUG.RETVARS%%

set RETURN=call set "DEBUG.ERRORLEVEL=%%ERRORLEVEL%%" %&% %MACRO% ( %\n%
  set DEBUG.EXITCODE=%!%MACRO.ARGS%!%%\n%
  if defined DEBUG.EXITCODE set DEBUG.EXITCODE=%!%DEBUG.EXITCODE: =%!%%\n%
  if not defined DEBUG.EXITCODE set DEBUG.EXITCODE=%!%DEBUG.ERRORLEVEL%!%%\n%
  set "DEBUG.SETARGS=" %\n%
  for %%v in (%!%DEBUG.RETVARS%!%) do ( %\n%
    set "DEBUG.VALUE=%'!%%%v%'!%" %# We must remove problematic characters in that value #% %\n%
    set "DEBUG.VALUE=%'!%DEBUG.VALUE:%%=%%DEBUG.percnt%%%'!%"   %# Remove percent #% %\n%
    for %%e in (sp tab cr lf quot) do for %%c in ("%'!%DEBUG.%%e%'!%") do ( %# Remove named character entities #% %\n%
      set "DEBUG.VALUE=%'!%DEBUG.VALUE:%%~c=%%DEBUG.%%e%%%'!%" %\n%
    ) %\n%
    set "DEBUG.VALUE=%'!%DEBUG.VALUE:^^=%%DEBUG.hat%%%'!%"      %# Remove carets #% %\n%
    call set "DEBUG.VALUE=%%DEBUG.VALUE:%!%=^^^^%%"             %# Remove exclamation points #% %\n%
    set "DEBUG.VALUE=%'!%DEBUG.VALUE:^^^^=%%DEBUG.excl%%%'!%"   %# Remove exclamation points #% %\n%
    set DEBUG.SETARGS=%!%DEBUG.SETARGS%!% "%%v=%'!%DEBUG.VALUE%'!%" %\n%
  ) %\n%
  if %!%DEBUG%!%==1 ( %# Build the debug message and display it #% %\n%
    set "DEBUG.MSG=return %'!%DEBUG.EXITCODE%'!%" %\n%
    for %%v in (%!%DEBUG.SETARGS%!%) do ( %\n%
      set "DEBUG.MSG=%'!%DEBUG.MSG%'!% %%DEBUG.amp%% set %%v" %!% %\n%
    ) %\n%
    call set "DEBUG.MSG=%'!%DEBUG.MSG:%%=%%DEBUG.excl%%%'!%" %# Change all percent to ! #%  %\n%
    if defined ^^%>%DEBUGOUT ( %# If we use a debugging stream distinct from stdout #% %\n%
      call :Echo.Eval2DebugOut %!%DEBUG.MSG%!%%# Use a helper routine, as delayed redirection does not work #%%\n%
    ) else ( %# Output directly here, which is faster #% %\n%
      for /f "delims=" %%c in ("%'!%INDENT%'!%%'!%DEBUG.MSG%'!%") do echo %%c%# Use a for loop to do a double !variable! expansion #% %\n%
    ) %\n%
    if defined LOGFILE ( %# If we have to send a copy to a log file #% %\n%
      call :Echo.Eval2LogFile %!%DEBUG.MSG%!%%# Use a helper routine, as delayed redirection does not work #%%\n%
    ) %\n%
  ) %\n%
  for %%r in (%!%DEBUG.EXITCODE%!%) do ( %# Carry the return values through the endlocal barriers #% %\n%
    for /f "delims=" %%a in (""" %'!%DEBUG.SETARGS%'!%") do ( %# The initial "" makes sure the body runs even if the arg list is empty #% %\n%
      endlocal %&% endlocal %&% endlocal %# Exit the RETURN and FUNCTION local scopes #% %\n%
      if "%'!%%'!%"=="" ( %# Delayed expansion is ON #% %\n%
        set "DEBUG.SETARGS=%%a" %\n%
        call set "DEBUG.SETARGS=%'!%DEBUG.SETARGS:%%=%%DEBUG.excl%%%'!%" %# Change all percent to ! #%  %\n%
        for %%v in (%!%DEBUG.SETARGS:~3%!%) do ( %\n%
          set %%v %# Set each upvar variable in the caller's scope #% %\n%
        ) %\n%
        set "DEBUG.SETARGS=" %\n%
      ) else ( %# Delayed expansion is OFF #% %\n%
        set "DEBUG.hat=^^^^" %# Carets need to be doubled to be set right below #% %\n%
        for %%v in (%%a) do if not %%v=="" ( %\n%
          call set %%v %# Set each upvar variable in the caller's scope #% %\n%
        ) %\n%
        set "DEBUG.hat=^^" %# Restore the normal value with a single caret #% %\n%
      ) %\n%
      exit /b %%r %# Return to the caller #% %\n%
    ) %\n%
  ) %\n%
) %/MACRO%
goto :eof

:# Echo and log a string, indented at the same level as the debug output.
:Echo
echo.%INDENT%%*
:Echo.Log
if defined LOGFILE %>>LOGFILE% echo.%INDENT%%*
goto :eof

:Echo.Eval2DebugOut %*=String with variables that need to be evaluated first
:# Must be called with delayed expansion on, so that !variables! within %* get expanded
:Echo.2DebugOut %*=String to output to the DEBUGOUT stream
%>DEBUGOUT% echo.%INDENT%%*
goto :eof

:Echo.Eval2LogFile %*=String with variables that need to be evaluated first
:# Must be called with delayed expansion on, so that !variables! within %* get expanded
:Echo.2LogFile %*=String to output to the LOGFILE
%>>LOGFILE% echo.%INDENT%%*
goto :eof

:#----------------------------------------------------------------------------#
:# Factorial routine, to test the tracing framework indentation

:Fact
%FUNCTION% enableextensions enabledelayedexpansion
%UPVAR% RETVAL
set N=%1
if .%1.==.. set N=0
if .%N%.==.0. (
  set RETVAL=1
) else (
  set /A M=N-1
  call :Fact !M!
  set /A RETVAL=N*RETVAL
)
%RETURN%

:Fact.test
%FUNCTION%
call :Fact %*
%ECHO% %RETVAL%
%RETURN%

:#----------------------------------------------------------------------------#
:# Main routine

:Main
echo # with debug off
set "DEBUG="
call :Fact.test 5

echo # with debug on
set "DEBUG=1"
call :Fact.test 5

Post Reply