Some tricks with undefined variables

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
jeb
Expert
Posts: 917
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Some tricks with undefined variables

#1 Post by jeb » 13 Sep 2019 03:33

Hi,

1) Default values
undefined variables are more or less empty variables, but only inside batch files.
The difference for batch files and command line

Code: Select all

echo var: %undefinedVar%

Output when executed on the command line:
var: %undefinedVar%

Output when executed in a batch file
var:
But even in batch files it's annoying that "echo %undefinedVar%" results into an output of "ECHO IS OFF" instead of an empty line.

Sometimes it would be usefull to has a replacement or default value for undefined variables, this can be done with a simple construct

Code: Select all

echo var: %undefinedVar:myDefaultValue%=%

Output:
var: myDefaultValue
It has limits, the default value has to be at least one character long and not all characters are allowed, like percent/equal signs.

2) Forcing Errors with text
While playing with macros, I got sometimes unexpected errors, like "The command resultLen is unknown ...".
Then I searched for the cause and found a line like

Code: Select all

%$strlen% resultLen myString
And later I discovered that $strLen was undefined, because something went wrong in the initializing process.

To catch problems like this, I could add lines like

Code: Select all

if not defined $strLen echo ERROR: Macro is not defined
But I build another solution,

Code: Select all

%$strlen:(#)ERROR_Macro_is_undefefined---%=% resultLen myString
When $strlen is undefined, an error occoured and the batch stops.

Code: Select all

"ERROR_Macro_is_undefefined---" kann syntaktisch an dieser Stelle nicht verarbeitet werden.
And if $strLen is defined, it works as expected, the complete part "(#)ERROR_Macro_is_undefefined---%=" is ignored.

But this technic has some limits.
- The error text can't contain percent signs nor equal signs.
- The text can't contain raw delimiters like "<space>;,=", they have to be escaped
- It only works for macros, because the (#) construct only creates a syntax error when it parsed like a command

It fails, if it isn't used as a command, it simply output the text

Code: Select all

echo %myVariable:(#)ERROR_Macro_is_undefefined---%=% 
...
Outut:
(#)ERROR_Macro_is_undefefined---
But even that can be solved by using another syntax error construct, like

Code: Select all

echo %myVariable:-%~*------- ERROR: macro is undefefined ------=%

Output:
Die folgende Verwendung des Pfadoperators zur Ersetzung eines Batchparameters
ist ungültig: %~*------- ERROR: macro is undefefined ------=%


Geben Sie CALL /? oder FOR /? ein, um herauszufinden, welche Formate gültig
sind.
It looks not so nice, but still it can be used when necessary.

2b) Loading macro when not defined
I'm using 2) also for my LOAD_ONCE macro

Code: Select all

%$LIB_LOAD_ONCE:call "%~dp0\libBase.cmd" "%~f0" :=%
It only calls libBase.cmd when $LIB_LOAD_ONCE isn't already defined.

The $LIB_LOAD_ONCE macro itself allows a batch file to execute commands only once, even when the same batch file is called later again (In my case it happens when libraries include other libraries)

Code: Select all

:define_$LIB_LOAD_ONCE
rem %$MACRO_PREPARE% $LIB_LOAD_ONCE
(set \n=^^^

)
if "%LIBRARY_DEBUG%" GTR "0" (
	set "_LIBRARY_DEBUG1.tmp=call echo Load library %%~n0, ready it's already loaded"
	set "_LIBRARY_DEBUG2.tmp=call echo Load library %%~n0"
)

REM *** In debug mode, show the loading of libBase
%_LIBRARY_DEBUG2.tmp%


REM *** This wraps the exclamation mark, works independent if delayed expansion is enabled or disabled
FOR /F %%! in ("! ! ^^^!") DO ^
set ^"$LIB_LOAD_ONCE=(%\n%
	setlocal EnableDelayedExpansion	%\n%
	set "path="%\n%
	set "pathExt=;"%\n%
	call set "libVarname=$%%~n0.loaded" %\n%
	FOR /F "delims=" %%^^L in ("%%!libVarname%%!") DO ( %\n%
		endlocal                 				%\n%
		if defined %%~^^L (    					%\n%
			%= library is already loaded =%		%\n%
			%_LIBRARY_DEBUG1.tmp%				%\n%
			exit /b                  			%\n%
		) else (                     			%\n%
			set "%%~L=1" 		    			%\n%
			%= library needs to be loaded =%	%\n%
			%_LIBRARY_DEBUG2.tmp%				%\n%
		)                            			%\n%
	)                            				%\n%
) "

exit /b
3) Independent exclamation marks
Sometimes you want to echo , set a variable or define a macro with exclamation marks, but you don't know the state of delayed expansion.
That's a problem, because normally the definitions are different.

Code: Select all

setlocal DisableDelayedExpansion
set "var1=Containg two exlamation marks, one normal ! and one quoted "!"" 
echo Two exlamation marks, one normal ! and one quoted "!"
setlocal EnableDelayedExpansion
set "var2=Containg two exlamation marks, one normal ^! and one quoted "^^!"" 
echo Two exlamation marks, one normal ^^! and one quoted "^!"
set var
But with a FOR-trick this can be solved

Code: Select all

setlocal DisableDelayedExpansion
call :test
setlocal EnableDelayedExpansion
call :test
exit /b

:test
FOR /F %%! in ("! ! ^^^!") DO (
	set "var=Containing two exlamation marks, one normal %%! and one quoted "%%!"" 
	echo Two exlamation marks, one normal %%! and one quoted "%%!"
	set var
)
exit /b
The FOR /F sets the %%! parameter to a single "!" in the case, when delayed expansion is disabled, because the string "! ! ^^^!" will be split at the first space.
But when delayed expansion is enabled, then the string is expanded to simply "^!" and that goes into the %%! parameter.
And in the echo/set command, the %%! expansion occours after the sepcial character phase but before the delayed expansion phase.
Therefore the "^!" results into a single "!", that's all.

3b) Exclamation marks and disappearing quotes
A small extension to 3)
Sometimes you want to echo variables containing closing parenthesis, like in "C:\Program Files (x86)", but it fails inside code blocks.
I could be solved by enclosing the expression into quotes, but then the quotes are also visible.

Or you use quotes that disappears!

Code: Select all

setlocal DisableDelayedExpansion
call :test
setlocal EnableDelayedExpansion
call :test
exit /b

:test
FOR /F "tokens=1,2 " %%! in ("#") DO FOR /F %%! in ("! ! ^^^!") DO (
	echo %%"Show &|<> and C:\Program Files (x86) without quotes %%!
)
exit /b
The outer loop defines two parameters %%! contains "#" and %%" is empty.
The inner loop overlays the definition of %%! to "!" or "^!".

Using %%" works like a quote, it escapes all characters in the special character phase, but later it expands to nothing

4) Conditional FOR parameter
Sometimes (okay, normally you want this only when defining macros) you would like content in a FOR parameter depending on a variable.

Code: Select all

@echo off

set "DEBUG="
call :test
set "DEBUG=no it's defined"
call :test
exit /b

:test
FOR /F "tokens=1,2" %%5 in ("X %DEBUG%") DO ^
FOR /F "tokens=2-4" %%5 in ("%%~6 "^;" "" "^;"") DO ^
FOR /F "delims=" %%5 in (^"^
%%~5"Text in case of UNDEFINED "^
%= Empty line, used to create a line feed =%
%%~6"Text in case when DEBUG is DEFINED, can even contain: %~f0""
) DO (
echo %%~5
)
exit /b
Output:

Code: Select all

Text in case of UNDEFINED
Text in case when DEBUG is DEFINED, can even contain: c:\temp\t5.bat

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

Re: Some tricks with undefined variables

#2 Post by jfl » 01 Oct 2019 06:45

Thanks jeb for these nice tricks! The first one really baffled me :shock:
Five years ago, I thought I knew everything about batch, and I keep learning new tricks here! :D

About trick 3):
It's worth defining a macro for easy reuse:

Code: Select all

@echo off
setlocal DisableDelayedExpansion

set ECHO!=FOR /F "tokens=1,2 " %%! in ("#") DO FOR /F %%! in ("! ! ^^^!") DO ECHO

%ECHO!% %%"Show &|<> and C:\Program Files (x86) without quotes %%!%%"
setlocal EnableDelayedExpansion
%ECHO!% %%"Show &|<> and C:\Program Files (x86) without quotes %%!%%"
exit /b
Note however that I rarely have problems with ! or () like you report, because (contrary to what's recommended by many on this forum) I'm working most of the time with DelayedExpansion enabled.

About trick 4):
I don't understand the need. Wouldn't it be much easier to use a simple if ?
Ex:

Code: Select all

if defined DEBUG (
  echo Text in case when DEBUG is DEFINED, can even contain: %~f0
) else (
  echo Text in case of UNDEFINED
)

dbenham
Expert
Posts: 2283
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: Some tricks with undefined variables

#3 Post by dbenham » 01 Oct 2019 10:08

@jfl

Nice ECHO! macro :!: 8)

I think 4) is useful when you want the definition of a macro to change depending on the state of some variable, for example if DEBUG is defined (enabled) or not. It allows incorporation of conditional text without creation of temp variables or staged definition

@jeb

I really like 1)
It is a clever practical utilization of the odd expansion rules that I can envision using some day.

Most of the others I also see how they can be useful.

But I don't understand the purpose of 2b)
I see how it works, but I don't understand why you would want to have code with a %$macro% "call" that defines the $macro if it is not yet defined, or executes the $macro if it is defined.
I can see how the undefined case could be useful if it both defined the $macro and then executed it. But that is not what happens.
I'm guessing you have some construct in your macro library that makes 2b) useful, but I'm not seeing it.


Dave Benham

jeb
Expert
Posts: 917
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Some tricks with undefined variables

#4 Post by jeb » 01 Oct 2019 14:28

dbenham wrote:
01 Oct 2019 10:08
@jfl

Nice ECHO! macro

I think 4) is useful when you want the definition of a macro to change depending on the state of some variable, for example if DEBUG is defined (enabled) or not. It allows incorporation of conditional text without creation of temp variables or staged definition
1- Me too, nice macro :D
2- Yes, Dave is right, I used it exactly in the DEBUG case, without the need of additional variables.
dbenham wrote:
01 Oct 2019 10:08
But I don't understand the purpose of 2b)
I see how it works, but I don't understand why you would want to have code with a %$macro% "call" that defines the $macro if it is not yet defined, or executes the $macro if it is defined.
I can see how the undefined case could be useful if it both defined the $macro and then executed it. But that is not what happens.
I'm guessing you have some construct in your macro library that makes 2b) useful, but I'm not seeing it.
Yes, each of my library files starts with the same code

Code: Select all

@echo off
REM *** Trampoline jump for function calls of the form ex. "C:\:libraryFunction:\..\temp\libThisLibraryName.cmd"
FOR /F "tokens=3 delims=:" %%L in ("%~0") DO goto :%%L

%$LIB_LOAD_ONCE:call "%~dp0\libBase.cmd" "%~f0" :=%
...
REM *** Include other necessary libraries
...
REM *** Specific code, to define the library macros
And you are right, when the $LIB_LOAD_ONCE macro needs to be defined, then it also executes the code at the end.

Code: Select all

:define_$LIB_LOAD_ONCE
rem %$MACRO_PREPARE% $LIB_LOAD_ONCE
(set \n=^^^

)
if "%LIBRARY_DEBUG%" GTR "0" (
	set "_LIBRARY_DEBUG1.tmp=call echo Load library %%~n0, ready it's already loaded"
	set "_LIBRARY_DEBUG2.tmp=call echo Load library %%~n0"
)

REM *** In debug mode, show the loading of libBase
%_LIBRARY_DEBUG2.tmp%

REM *** This wraps the exclamation mark, works independent if delayed expansion is enabled or disabled
FOR /F %%! in ("! ! ^^^!") DO ^
set ^"$LIB_LOAD_ONCE=(%\n%
	setlocal EnableDelayedExpansion	%\n%
	set "path="%\n%
	set "pathExt=;"%\n%
	call set "libVarname=$%%~n0.loaded" %\n%
	FOR /F "delims=" %%^^L in ("%%!libVarname%%!") DO ( %\n%
		endlocal                 				%\n%
		if defined %%~^^L (    					%\n%
			%= library is already loaded =%		%\n%
			%_LIBRARY_DEBUG1.tmp%				%\n%
			exit /b                  			%\n%
		) else (                     			%\n%
			set "%%~L=1" 		    			%\n%
			%= library needs to be loaded =%	%\n%
			%_LIBRARY_DEBUG2.tmp%				%\n%
		)                            			%\n%
	)                            				%\n%
) "

rem *** Now mark the libBase and the caller of libBase as loaded
set "$%~n0.loaded=1"
( 
   (goto) 2> nul
   call set "$%%~n1.loaded=1"
)
exit /b
::: detector line %~* *** FATAL ERROR: missing parenthesis or exit /b
But it's even possible to write delayed macro loading, at first the macro contains only a few instructions.
But the first execution of the macro loads the macro and executes it.

But, I discarded it, as I can't see any speed benefits only some drawbacks.

Post Reply