Page 1 of 1

IF THEN syntactic sugar

Posted: 16 Feb 2013 17:34
by Queue
A quick preface: I enjoy batch's ubiquity, and so while I mostly work with 32-bit x86 assembly, C++ and the occasional interpreted language, I do a fair bit of goofy small projects with batch. Something that always bugged me was how strict batch's IF statement parenthesis were. Namely, I prefer the following format:

Code: Select all

if a > b
(
   echo Y
)
else
(
   echo N
)

but obviously this format doesn't work in batch. Less obviously, the following doesn't work either:

Code: Select all

if %a% gtr %b% ^
(
   echo Y
)^
else ^
(
   echo N
)

Line continuations work almost everywhere in batch, but for whatever reason the parser is much more picky with IF statements. Of note, )^ (before the else) does work.

Batch also lacks an OR statement to be used in IF statements (IF itself can substitute for AND).

And so, I present what I've been playing with for the past few days, some syntactic sugar to make batch IF statements more palatable, at least for myself.

I'll start with the structural var:

Code: Select all

set "^=(:)^

if a==a %^%
(
   echo Y
)^
else %^%
(
   echo N
)

How it works is simple:
within (:)^
  • the ( gives the IF statement the opening parenthesis it wants
  • the : fools the parser into ignoring the line and immediately advancing one line (in this case to the echo command)
  • the ) serves no real purpose other than taking up space; it could actually be nearly any character
  • the ^ is a line continuation so that the next line (the lone ( symbol) is ignored

The downside is that you have to affix %^% onto the end of each IF and ELSE line where you want the next line to contain a dummy ( symbol, and for the ) before else you need to make it )^, but for me personally, I find it worthwhile.

Now, for something that might have broader appeal, I present the OR statement:

Code: Select all

set "^=(:)^
set "if=call;&(if
set "or=call)&&(if
set "and=if
set "then=call)&if;errorlevel;1

%if% a==a %and% a==b %or% %errorlevel%==0 %or% not a==a %then% %^%
(
   echo Y
)^
else %^%
(
   echo N
)

I use "call;" to set errorlevel to 0 and "call" (no space after the command) to set errorlevel to 1.

There are some obvious caveats:
  • the current errorlevel will be lost without adding code to restore it within both the IF and ELSE blocks
  • a specialized %if% is required to start the statement rather than just IF
  • a specialized %then% is required to finish the statement rather than nothing
  • if you want to test the errorlevel within the IF statement, you have to use a %errorlevel% comparison instead of the special errorlevel statement

I haven't yet exhaustively tested this, but it's held up in every instance where I've used it so far and is more or less what I'd like to have a dialogue about.

Some bonus tidbits:

Code: Select all

set "&&=||:&if;not;errorlevel;1;
set "||=||:&if;errorlevel;1;

:: which are used like
find "NVIDIA GeForce 7900 GS" "..\d3d9.dll" %&&% %^%
(
   set v.FAKE=NVIDIA GeForce 7900 GS
)^
else find "ENB" "..\d3d9.dll" %&&% %^%
(
   set v.FAKE=NVIDIA GeForce 9600 GT
)^
else %^%
(
   set v.FAKE=
)

Code: Select all

:: predecessors to the later version
set "or=call)&(if
set "or=call)&(if;not;errorlevel;1;if

Code: Select all

:: inverted use of errorlevel that DOES NOT WORK
set "if=call&(if
set "or=call;)||(if
set "then=call;)&if;not;errorlevel;1

Code: Select all

:: failed attempts at standardized parenthesis
set "(=else;(
set ")=)else^

:: which led me to discover that
if a==b (echo Y)^
else^
else^
else (echo N)
:: works just fine; all those ELSEs get compacted into a single ELSE


And finally, a big fat block of batch I put at the start of my most recent batch file to make sure things will work as I expect:

Code: Select all

if not n^t==nt exit
set ~x9=
if "%~x9%~x9"=="~x9" setlocal enableextensions
if "%~x9%~x9"=="~x9" exit
if not cmdextversion 2 exit
if "!~x9!~x9"=="~x9" setlocal disabledelayedexpansion
if "!~x9!~x9"=="~x9" exit
if not defined COMSPEC exit
if not defined TEMP exit
if not defined USERPROFILE exit
if not defined CD exit
2>nul set CD && exit || call;
2>nul set ERRORLEVEL && exit || call;


Queue

Re: IF THEN syntactic sugar

Posted: 17 Feb 2013 14:12
by dbenham
Very nice work :D

I haven't seen CALL used like that to set or clear the ERRORLEVEL. That is very effective... and fast. Most uses of CALL are relatively slow, but this is not.

When clearing the ERRORLEVEL, extra spaces after CALL; are not a problem. The semicolon is not needed - you can also use a space, comma, tab, or equal.

But when setting the ERRORLEVEL to 1 with CALL, extra trailing spaces are a problem.

You might consider parentheses to ensure the command is setting ERRORLEVEL appropriately
- to set ERRORLEVEL TO 0: (CALL ), or (CALL;)
- to set ERRORLEVEL TO 1: (CALL)

I've never minded the fact that parentheses must be on the same line as ELSE, but your AND and OR are interesting. I get confused by the order of operation. I don't think I would ever use either construct unless there was a method to add parentheses to explicitly set the order of operation. It would probably require variables for the parentheses.


Dave Benham

Re: IF THEN syntactic sugar

Posted: 17 Feb 2013 23:47
by Queue
Using call for the errorlevel was a happy accident. When I originally set out to do this, my ideal result had been a stand-alone %or%, so I was trying all sorts of terrible things to break the parser (if only ascii 0x1A could also work its magic via environment variable!). At one point, I was trying to call some internal command mess and was seeing if there was any way to avoid the file system search (yes, I've seen the thread(s) on this forum related to such), and had wound up trying to call some stuff wrapped in parenthesis (which wasn't working). That evolved into call(:), which evolved into call(:, then I realized what was happening and settled on call;. Previously I'd been using type nul to set errorlevel to 0. Using an argument-less call to get an errorlevel of 1 was a sort of convergent evolution as I looked for something short and simple; I'd initially used color 00 but that's long and requires the delimiter to be a space (color;00 fails, for example), set= works but sends a string to stderr, and when I was shortening my errorlevel 0 call; I'd of course tried call by itself so when I got around to replacing color 00, call seemed like the logical choice.

All of that said, I've only tried call and call; (or I suppose, less ambiguously, (call) and (call;)) on XP SP3. I presume they won't work under command.com on 9x and just haven't tested on Vista/7/8 yet. They were tangential to my goal and just happened to be compact.

In regards to operator precedence, it's left-to-right, %and% having precedence over %or%, and pretty inflexible, currently. It also stops evaluating further conditions after one matches, thanks to the use of && within the %or% var. If nothing else it's predictable and consistent. What I'd like to do though is incorporate support for using any internal command or external executable with a parse-able return between the %if% and %then%, but it's currently only set up for IF statements (except if errorlevel #).

For a simple example of how I'm using it:

Code: Select all

%if% not exist "7z.exe" %or% not exist "7z.dll" %then% call:i_1 "%~0"

where originally I had:

Code: Select all

if not exist "7z.exe" ( call:i_1 "%~0"
) else if not exist "7z.dll" call:i_1 "%~0"


Queue

Re: IF THEN syntactic sugar

Posted: 18 Feb 2013 07:15
by jeb
Queue wrote:Line continuations work almost everywhere in batch, but for whatever reason the parser is much more picky with IF statements. Of note, )^ (before the else) does work.

Ok, this can be explained.

You wrote syntactically the same as

Code: Select all

if 1==1 ^(echo Y)


if 1==1 ^
( echo Y )

This fails, as the opening bracket is escaped and the parser expects this must be a command.

The problem is here, you can't do a line continuation without confusion the IF-parser.

Code: Select all

if 1== 1 ^
 ( echo Fails also)

This fails, as now the space is escaped and handled like a command.

Only some strange things like this works

Code: Select all

if 1==1 ^
rem. & ( echo Y)

But this seems to be really bad.

Your CALL solution looks very interessting, it's a nice idea to use it this way.

jeb

Re: IF THEN syntactic sugar

Posted: 02 Mar 2013 21:25
by DigitalSnow
Fascinating post! Thanks for sharing the explanation and implementation. The call errorlevel trick is definitely something I will remember. and I can see the %&&% being handy.