Page 4 of 4

Re: foolproof counting of arguments

Posted: 28 Aug 2018 10:27
by pieh-ejdsch
instead of start "" / b ...
what happens if you
do it without /b?

Phil

Re: foolproof counting of arguments

Posted: 29 Aug 2018 16:26
by sst
jeb wrote:
28 Aug 2018 08:05
It could be useful when someone found a solution to some of the above points, or even how to close the current cmd window :?:
Nice trick jeb

The main thread can be terminated, but this is reasonable only when batch file called from command line and not from another batch file.
The trick is to redirect the stdin to a write-only stream, when it backs to prompt it can't read from stdin and will be terminated immediately and the handle to params.tmp will be closed then it can be deleted in StayAlive.

Code: Select all

@echo off
REM *** Thread redirector 
for /F "tokens=3 delims=:" %%F in ("%~0") do goto %%F

cls

REM *** Clear params.tmp
break > params.tmp

start "" /b cmd /c "%~d0\:StayAlive:\..\%~pnx0" params.tmp

(set LF=^
%=empty=%
)
REM *** Change prompt for better recognition
prompt #PROMPT#


REM *** Change streams permanently
REM *** stream1 redirects to params.tmp
REM *** stream2 redirects to nul
;;;;; echo on >nul 2>nul 0>nul 3>params.tmp 4>nul 5>&3


@REM *** This is the magic part, it forces a syntax error, the error message itself shows the expanded %asterix without ANY modification
( Prepare ) PARAMS:%LF%%*%LF%

echo Works
exit /b


REM *** Second thread to fetch and show the parameters
:StayAlive

:__WaitForParams
if %~z1 EQU 0 (
    goto :__WaitForParams
)
REM *** Show the result
findstr /n "^" %1
del %1
echo I'm alive, parent dead :(
pause
exit

Re: foolproof counting of arguments

Posted: 30 Aug 2018 03:41
by jeb
pieh-ejdsch wrote:
28 Aug 2018 10:27
instead of start "" / b ...
what happens if you
do it without /b?
It opens a new cmd window, but that doesn't solve the problem that the main thread is still open.
sst wrote:
29 Aug 2018 16:26
The main thread can be terminated, but this is reasonable only when batch file called from command line and not from another batch file.
The trick is to redirect the stdin to a write-only stream, when it backs to prompt it can't read from stdin and will be terminated immediately and the handle to params.tmp will be closed then it can be deleted in StayAlive.
:D That's a cool trick, I didn't know.

I changed the start cmd /c to cmd /k and I removed the exit from the stayAlive thread
That can be used to build a more "useable" solution.

Code: Select all

@echo off
REM *** Thread redirector 
for /F "tokens=3 delims=:" %%F in ("%~0") do goto %%F

REM *** Clear params.tmp
break > params.tmp

start "" /b cmd /k "%~d0\:StayAlive:\..\%~pnx0 params.tmp"

(set LF=^
%=empty=%
)
REM *** Change prompt for better recognition
prompt #PROMPT#


REM *** Change streams permanently
REM *** stream1 redirects to params.tmp
REM *** stream2 redirects to nul
echo on >nul 2>nul 0>nul 3>params.tmp 4>nul 5>&3

@REM *** This is the magic part, it forces a syntax error, the error message itself shows the expanded %asterix without ANY modification
( Prepare ) PARAMS:%LF%%*%LF%

echo Works
exit /b


REM *** Second thread to fetch and show the parameters
:StayAlive

:__WaitForParams
if %~z1 EQU 0 (
    goto :__WaitForParams
)
REM *** Show the result
findstr /n "^" %1 
A small test with a mutiline paramter.
output wrote:c:\temp\Parser>GetParamFull.bat Line1^
Mehr?
Mehr? () Line2^
Mehr?
Mehr? caret^^ Line3
1:
2:#PROMPT#( Prepare ) PARAMS:
3:Line1
4:() Line2
5:caret^ Line3
6:
7:
8:#PROMPT#
c:\temp\Parser>

Re: foolproof counting of arguments

Posted: 30 Aug 2018 16:11
by dbenham
Very cool technique with surprising behavior. :!: 8)

It is very odd that the extra label is required. I get the wrong prompt in the output if I consolidate the thread redirection label and loop label into a single label. :?

I extended the method to capture the binary image of the parameter string in an ARGS variable via CERTUTIL.

Code: Select all

@echo off
REM *** Thread redirector
for /F "tokens=3 delims=:" %%F in ("%~0") do goto %%F

REM *** Clear params.tmp
break > params.tmp

start "" /b cmd /k "%~d0\:GetParms:\..\%~pnx0 params.tmp"

REM *** Change prompt to achieve fixed length prefix and suffix (5 bytes each)
prompt #


REM *** Change streams permanently
REM *** stream1 and stream0 redirect to params.tmp
REM *** stream2 redirects to nul
echo on >nul 2>&1 0>&1 3>params.tmp 4>&1 5>&3

@REM *** This is the magic part, it forces a fatal syntax error, the error message itself shows the expanded %asterix without ANY modification
()%*

REM *** This is never reached, and the parent command shell terminates because stdin is invalid (an output file)


REM *** Second thread to fetch parameters as one string within args variable
:GetParms
:WaitForParmsLoop
if %~z1 EQU 0 (
  goto :WaitForParmsLoop
)
setlocal enableDelayedExpansion

set "B=^!"
set "C=^"

(set L=^
%= Stores a LineFeed =%
)

for /F %%Z in ('copy /Z "%~dpf0" nul') do set "R=%%Z"  %= Stores a CarriageReturn =%

REM *** Convert binary tmp file to hex
certutil -f -encodehex %1 %~n1.hex 4 >nul

REM *** Encode hex ! ^ LF CR as hex !B! !C! !L! !R! and write modified hex file
>%~n1.hex2 (
  for /f "delims=" %%A in (%~n1.hex) do for %%B in (%%A) do (
    set "char=%%B"
    set "char=!char:21=21 42 21!"
    set "char=!char:5e=21 43 21!"
    set "char=!char:0a=21 4c 21!"
    set "char=!char:0d=21 52 21!"
    echo !char!
  )
)

REM *** Convert modified hex to encoded binary
certutil -f -decodehex %~n1.hex2 %1 >nul

REM *** Read the encoded binary and decode into args variable
for /f delims^=^ eol^= %%A in (%1) do set "args=%%A"

REM *** Remove the unwanted prefix and suffix
set "args=!args:~5,-5!

REM *** Cleanup temp files
del %1 %~n1.hex %~n1.hex2

REM *** We now have the exact parameter string stored in args. Ready to begin processing
echo [!args!]
As written, the code clobbers variables L R B C and CHAR, in addition to setting ARGS. I suppose one of the safe return techniques can be used to make those variables temporary.

There is still one nasty potential problem with the technique - It will fail miserably if any of the streams 3 and/or 4 and/or 5 are already defined when the script is called due to prior redirection.

If I can determine the lowest 3 available streams, then the technique could be modified to always work as long as there are 3 available streams within the range 3 - 9. I think I know how to do it, but it will have undesired screen output that I don't know how to avoid. I don't have time to develop my idea yet. I'll post when I get time, or someone else can beat me to it.


Dave Benham

Re: foolproof counting of arguments

Posted: 30 Aug 2018 23:10
by dbenham
I managed to do it :)

The code below figures out the first 3 available unused file handles to use for the permanent redirection, and it fails cleanly if there are not 3 available handles in the range 3 - 9.

As I feared, I was not able to prevent an unwanted error message on the screen when detecting the lowest available handle.

Code: Select all

@echo off
REM *** Thread redirector
for /F "tokens=3 delims=:" %%F in ("%~0") do goto %%F

REM *** Clear params.tmp
break > params.tmp

REM *** Define permanent redirection
REM *** handles 0 and 1 redirect to params.tmp
REM *** handle 2 redirects to nul
call :redirect || (
  >&2 echo Unable to permanently redirect handles 0 1 and 2
  exit /b 1
)
REM *** Activate the CLS below to "hide" the unwanted error message
:: CLS

REM *** Launch 2nd process to gather parameters and do main processing
start "" /b cmd /k "%~d0\:GetParms:\..\%~pnx0 params.tmp"

REM *** Change prompt to achieve fixed length prefix and suffix (5 bytes each)
prompt #

REM *** Execute the permanent redirection
echo on %redirect%

@REM *** This is the magic part, it forces a fatal syntax error, the error message itself shows the expanded %asterix without ANY modification
()%*

REM *** This is never reached, and the parent command shell terminates because stdin is invalid (an output file)

:redirect
setlocal

:: Find Highest unused handle
set "high="
call :test1 8 9 high & if defined high goto :findSecond
for %%A in (8 7 6 5) do call :test1 9 %%A high & if defined high goto :findSecond
exit /b 1

:findSecond
for /l %%A in (4 1 %high%) do call :test1 %high% %%A second & if defined second goto :findThird

:findThird
for /l %%A in (%second% 1 %high%) do call :test1 %second% %%A third & if defined third goto :findFirst

:test1
if %1 neq %2 (2>nul (%1>&%2 break))||set "%3=%2"
exit /b

:findFirst
for /l %%A in (3 1 %second%) do call :test2 %%A && (
  endlocal
  set "redirect=>nul 2>&1 0>&1 %%A>params.tmp %second%>&1 %third%>&%%A"
  exit /b 0
)

:test2
(%second%>&%1 break)||exit /b 0
exit /b 1


REM *** Second thread to fetch parameters as one string within args variable
:GetParms
:WaitForParmsLoop
if %~z1 EQU 0 (
  goto :WaitForParmsLoop
)
setlocal enableDelayedExpansion

set "B=^!"
set "C=^"

(set L=^
%= Stores a LineFeed =%
)

for /F %%Z in ('copy /Z "%~dpf0" nul') do set "R=%%Z"  %= Stores a CarriageReturn =%

REM *** Convert binary tmp file to hex
certutil -f -encodehex %1 %~n1.hex 4 >nul

REM *** Encode hex ! ^ LF CR as hex !B! !C! !L! !R! and write modified hex file
>%~n1.hex2 (
  for /f "delims=" %%A in (%~n1.hex) do for %%B in (%%A) do (
    set "char=%%B"
    set "char=!char:21=21 42 21!"
    set "char=!char:5e=21 43 21!"
    set "char=!char:0a=21 4c 21!"
    set "char=!char:0d=21 52 21!"
    echo !char!
  )
)

REM *** Convert modified hex to encoded binary
certutil -f -decodehex %~n1.hex2 %1 >nul

REM *** Read the encoded binary and decode into args variable
for /f delims^=^ eol^= %%A in (%1) do set "args=%%A"

REM *** Remove the unwanted prefix and suffix
set "args=!args:~5,-5!

REM *** Cleanup temp files
del %1 %~n1.hex %~n1.hex2

REM *** We now have the exact parameter string stored in args. Ready to begin processing
echo [!args!]
Here are some sample results demonstrating that it works with prior redirection, unless 3 are not available
output wrote: C:\test>3>nul 5>nul 7>nul 8>nul GetFullParameter Line 1^
More?
More? Line 2 ^^ ! ^& ^> ^<^
More?
More? Line 3
The handle could not be duplicated
during redirection of handle 6.
[Line 1
Line 2 ^ ! & > <
Line 3]

C:\test>3>nul 5>nul 7>nul 8>nul 9>nul GetFullParameter test
Unable to permanently redirect handles 0 1 and 2

Dave Benham

Re: foolproof counting of arguments

Posted: 31 Aug 2018 03:01
by siberia-man
@dbenham

I tested the last example. That's output I didn't expect to see

Code: Select all

C:\Temp>z "1 2" "3"
The handle could not be duplicated
during redirection of handle 4.
["1 2" "3]

C:\Temp>z "1 2" 3
The handle could not be duplicated
during redirection of handle 4.
["1 2]
One more thing that seems not good. The command history is partially lost after executing the script.

Re: foolproof counting of arguments

Posted: 31 Aug 2018 05:42
by jeb
dbenham wrote:
30 Aug 2018 16:11
It is very odd that the extra label is required. I get the wrong prompt in the output if I consolidate the thread redirection label and loop label into a single label. :?
I can't see any problem with deleting the :__WaitForParams label, it still works for me.
siberia-man wrote:
31 Aug 2018 03:01
One more thing that seems not good. The command history is partially lost after executing the script.
That is expected, as the main thread will be closed and only the new created "StayAlive" thread will continue.
Thats currently necessary, because the main thread has permanently changed it's stdout/stderr stream.
dbenham wrote:
30 Aug 2018 23:10
I managed to do it

The code below figures out the first 3 available unused file handles to use for the permanent redirection, and it fails cleanly if there are not 3 available handles in the range 3 - 9.
That's a cool technic, too.

But I would like to find a solution without the need of the "permanent redirect" technic.

It's easy to redirect the output of a fatal syntax error by redirecting the output of a call.

Code: Select all

call :fatalError > params.txt 2> nul

:fatalError
@echo on
()%*
The problem is here, that %* doesn't contain the batch start parameters, instead it contains the parameters of the CALL.
I thought of using the "(goto) 2> nul" trick here, but then I always lose the redirection.

Code: Select all

@echo off

call :fatalError > params.txt 2> nul

:fatalError
@echo on
(
  (goto) 
  goto :__fatal
)

:__fatal
()%*

Re: foolproof counting of arguments

Posted: 31 Aug 2018 08:19
by dbenham
jeb wrote:
31 Aug 2018 05:42
dbenham wrote:
30 Aug 2018 16:11
It is very odd that the extra label is required. I get the wrong prompt in the output if I consolidate the thread redirection label and loop label into a single label. :?
I can't see any problem with deleting the :__WaitForParams label, it still works for me.
:shock: You are correct :!:
It works with a single label now. I swear I was seeing different behavior at one point, but now I can't reproduce it. When it wasn't working, the first captured prompt line had the expected "#", but the second captured prompt line had "C:\test>".

I simplified the GetParams code to use a single label instead of two.

Code: Select all

@echo off
REM *** Thread redirector
for /F "tokens=3 delims=:" %%F in ("%~0") do goto %%F

REM *** Clear params.tmp
break > params.tmp

REM *** Define permanent redirection
REM *** handles 0 and 1 redirect to params.tmp
REM *** handle 2 redirects to nul
call :redirect || (
  >&2 echo Unable to permanently redirect handles 0 1 and 2
  exit /b 1
)
REM *** Activate the CLS below to "hide" the unwanted error message
:: CLS

REM *** Launch 2nd process to gather parameters and do main processing
start "" /b cmd /k "%~d0\:GetParms:\..\%~pnx0 params.tmp"

REM *** Change prompt to achieve fixed length prefix and suffix (5 bytes each)
prompt #

REM *** Execute the permanent redirection
echo on %redirect%

@REM *** This is the magic part, it forces a fatal syntax error, the error message itself shows the expanded %asterix without ANY modification
()%*

REM *** This is never reached, and the parent command shell terminates because stdin is invalid (an output file)

:redirect
setlocal

:: Find Highest unused handle
set "high="
call :test1 8 9 high & if defined high goto :findSecond
for %%A in (8 7 6 5) do call :test1 9 %%A high & if defined high goto :findSecond
exit /b 1

:findSecond
for /l %%A in (4 1 %high%) do call :test1 %high% %%A second & if defined second goto :findThird

:findThird
for /l %%A in (%second% 1 %high%) do call :test1 %second% %%A third & if defined third goto :findFirst

:test1
if %1 neq %2 (2>nul (%1>&%2 break))||set "%3=%2"
exit /b

:findFirst
for /l %%A in (3 1 %second%) do call :test2 %%A && (
  endlocal
  set "redirect=>nul 2>&1 0>&1 %%A>params.tmp %second%>&1 %third%>&%%A"
  exit /b 0
)

:test2
if %second%==4 exit /b 0
(%second%>&%1 break)||exit /b 0
exit /b 1


REM *** Second thread to fetch parameters as one string within args variable
:GetParms
if %~z1 EQU 0 goto :GetParms
setlocal enableDelayedExpansion

set "B=^!"
set "C=^"

(set L=^
%= Stores a LineFeed =%
)

for /F %%Z in ('copy /Z "%~dpf0" nul') do set "R=%%Z"  %= Stores a CarriageReturn =%

REM *** Convert binary tmp file to hex
certutil -f -encodehex %1 %~n1.hex 4 >nul

REM *** Encode hex ! ^ LF CR as hex !B! !C! !L! !R! and write modified hex file
>%~n1.hex2 (
  for /f "delims=" %%A in (%~n1.hex) do for %%B in (%%A) do (
    set "char=%%B"
    set "char=!char:21=21 42 21!"
    set "char=!char:5e=21 43 21!"
    set "char=!char:0a=21 4c 21!"
    set "char=!char:0d=21 52 21!"
    echo !char!
  )
)

REM *** Convert modified hex to encoded binary
certutil -f -decodehex %~n1.hex2 %1 >nul

REM *** Read the encoded binary and decode into args variable
for /f delims^=^ eol^= %%A in (%1) do set "args=%%A"

REM *** Remove the unwanted prefix and suffix
set "args=!args:~5,-5!

REM *** Cleanup temp files
del %1 %~n1.hex %~n1.hex2

REM *** We now have the exact parameter string stored in args. Ready to begin processing
echo [!args!]
Note that the "stay-alive" thread inherits the REDIRECT variable, which is convenient for probing how the permanent redirection was achieved.

I also made a simple change that eliminates the unwanted error message if both handles 3 and 4 are available. If I determine that the 2nd available handle is 4, then I know that the 1st one must be 3.
Below are some sample runs:
Output wrote: C:\test>5>nul 6>nul 7>nul getFullParameter test
[test]

C:\test>set redirect
redirect=>nul 2>&1 0>&1 3>params.tmp 4>&1 8>&3

C:\test>4>nul 5>nul 6>nul 7>nul getFullParameter test
The handle could not be duplicated
during redirection of handle 8.
[test]

C:\test>set redirect
redirect=>nul 2>&1 0>&1 3>params.tmp 8>&1 9>&3
Note that leading and trailing spaces are stripped.
For example:

Code: Select all

[getFullParameter     test     ] yields [test].
But if the first and last characters are anything other than space, including other token delimiters like , ; = <tab> </xFF> <LF>, then all leading and trailing characters are preserved.
For example:

Code: Select all

[getFullParameter=     test     =] yields [=     test     =]
I found another limitation. The script does not work if called via CMD /C - Only the first character of the parameter is preserved.

But if CMD /K is used, then the full parameter on the first line is preserved. But subsequent lines are stripped, which is not surprising.

Either way, if CMD /C or CMD /K is used, then the console ends up with multiple active processes associated with it, which messes up subsequent processing within the console. So that means the script cannot be used with FOR /F or pipes.


jeb wrote:
31 Aug 2018 05:42
But I would like to find a solution without the need of the "permanent redirect" technic.
Good luck with that. I don't see a way forward. But then again, I never expected to see the permanent redirection solution either.


Dave Benham

Re: foolproof counting of arguments

Posted: 31 Aug 2018 08:34
by dbenham
jeb wrote:
31 Aug 2018 05:42
dbenham wrote:
30 Aug 2018 23:10
I managed to do it

The code below figures out the first 3 available unused file handles to use for the permanent redirection, and it fails cleanly if there are not 3 available handles in the range 3 - 9.
That's a cool technic, too.
Note that my algorithm only works if you are trying to identify the first 3 available handles.

I can modify the algorithm to locate the first 2 available handles when only 2 are available, but then all tests will have undesired stderr output.

If there is only 1 available handle, then I don't know how to identify it at all.


Dave Benham

Re: foolproof counting of arguments

Posted: 31 Aug 2018 08:53
by dbenham
siberia-man wrote:
31 Aug 2018 03:01
@dbenham

I tested the last example. That's output I didn't expect to see

Code: Select all

C:\Temp>z "1 2" "3"
The handle could not be duplicated
during redirection of handle 4.
["1 2" "3]

C:\Temp>z "1 2" 3
The handle could not be duplicated
during redirection of handle 4.
["1 2]
Ugh. I was missing a trailing quote in my last assignment. All fixed.

Code: Select all

@echo off
REM *** Thread redirector
for /F "tokens=3 delims=:" %%F in ("%~0") do goto %%F

REM *** Clear params.tmp
break > params.tmp

REM *** Define permanent redirection
REM *** handles 0 and 1 redirect to params.tmp
REM *** handle 2 redirects to nul
call :redirect || (
  >&2 echo Unable to permanently redirect handles 0 1 and 2
  exit /b 1
)
REM *** Activate the CLS below to "hide" the unwanted error message
:: CLS

REM *** Launch 2nd process to gather parameters and do main processing
start "" /b cmd /k "%~d0\:GetParms:\..\%~pnx0 params.tmp"

REM *** Change prompt to achieve fixed length prefix and suffix (5 bytes each)
prompt #

REM *** Execute the permanent redirection
echo on %redirect%

@REM *** This is the magic part, it forces a fatal syntax error, the error message itself shows the expanded %asterix without ANY modification
()%*

REM *** This is never reached, and the parent command shell terminates because stdin is invalid (an output file)

:redirect
setlocal

:: Find Highest unused handle
set "high="
call :test1 8 9 high & if defined high goto :findSecond
for %%A in (8 7 6 5) do call :test1 9 %%A high & if defined high goto :findSecond
exit /b 1

:findSecond
for /l %%A in (4 1 %high%) do call :test1 %high% %%A second & if defined second goto :findThird

:findThird
for /l %%A in (%second% 1 %high%) do call :test1 %second% %%A third & if defined third goto :findFirst

:test1
if %1 neq %2 (2>nul (%1>&%2 break))||set "%3=%2"
exit /b

:findFirst
for /l %%A in (3 1 %second%) do call :test2 %%A && (
  endlocal
  set "redirect=>nul 2>&1 0>&1 %%A>params.tmp %second%>&1 %third%>&%%A"
  exit /b 0
)

:test2
if %second%==4 exit /b 0
(%second%>&%1 break)||exit /b 0
exit /b 1


REM *** Second thread to fetch parameters as one string within args variable
:GetParms
if %~z1 EQU 0 goto :GetParms
setlocal enableDelayedExpansion

set "B=^!"
set "C=^"

(set L=^
%= Stores a LineFeed =%
)

for /F %%Z in ('copy /Z "%~dpf0" nul') do set "R=%%Z"  %= Stores a CarriageReturn =%

REM *** Convert binary tmp file to hex
certutil -f -encodehex %1 %~n1.hex 4 >nul

REM *** Encode hex ! ^ LF CR as hex !B! !C! !L! !R! and write modified hex file
>%~n1.hex2 (
  for /f "delims=" %%A in (%~n1.hex) do for %%B in (%%A) do (
    set "char=%%B"
    set "char=!char:21=21 42 21!"
    set "char=!char:5e=21 43 21!"
    set "char=!char:0a=21 4c 21!"
    set "char=!char:0d=21 52 21!"
    echo !char!
  )
)

REM *** Convert modified hex to encoded binary
certutil -f -decodehex %~n1.hex2 %1 >nul

REM *** Read the encoded binary and decode into args variable
for /f delims^=^ eol^= %%A in (%1) do set "args=%%A"

REM *** Remove the unwanted prefix and suffix
set "args=!args:~5,-5!"

REM *** Cleanup temp files
del %1 %~n1.hex %~n1.hex2

REM *** We now have the exact parameter string stored in args. Ready to begin processing
echo [!args!]
Thanks for reporting the bug.


Dave Benham

Re: foolproof counting of arguments

Posted: 01 Sep 2018 12:17
by sst
dbenham wrote:
30 Aug 2018 23:10
As I feared, I was not able to prevent an unwanted error message on the screen when detecting the lowest available handle.
I found a way to prevent the error message while detecting the available handles.

If we do the detection while stderr is redirected, then we just need to find the first 2 available handles. If we have 2 handles it means that there was room for the original stderr to sit on one the lower handles. We don't need to know in which handle stderr is backed up, we just need to to the permanent redirection in two steps.

For simplicity I used jeb's first proposed method for displaying the parameters, to focus more on the permanent redirection.

Code: Select all

@echo off
REM *** Thread redirector 
for /F "tokens=3 delims=:" %%F in ("%~0") do goto %%F

REM *** Clear params.tmp
break > params.tmp

start "" /b cmd /k @"%~d0\:StayAlive:\..\%~pnx0" "params.tmp"
prompt #


:: Detect available handles without displaying error messages
:: Continue only if there is at least 3 unused handles.
set /a "freeSlots=usedSlots=0"
2>nul (
    for /L %%A in (3,1,8) do call :enumIO 9 %%A
)
:: If non of the handles 4 to 8 are free then there are three possibilities for handle 9
:: 1. It is occupied before us (Impossible to detect)
:: 2. It is occupied by redirection of stderr (Impossible to detect)
:: 3. It is free (Impossible to detect)
:: either way we don't have the required free handles
if %freeSlots% NEQ 0 (
    2>nul call :enumIO 1 9
)
:: One of the handles was used by redirection of stderr
:: So we only need 2 additional free handles to continue.
if %freeSlots% LSS 2 (
    echo Not enough IO Slots.
    exit /b
)

:: First , do the permanent redirection of stderr. We don't have to know the backup handle.
:: The order of redirection is critical: stderr ---> freeSlots ---> usedSlots
set "stderr_permanent=break 2>nul"
for /L %%A in (%freeSlots%,-1,1) do call set "stderr_permanent=%%stderr_permanent%% %%freeIO[%%A]%%>&2"
for /L %%A in (%usedSlots%,-1,1) do call set "stderr_permanent=%%stderr_permanent%% %%usedIO[%%A]%%>&2"
%stderr_permanent%

set freeIO[
set usedIO[
set stderr_permanent

:: Next do a permanent redirection of stdout and stdin by known free handles.
echo on >params.tmp 0>nul %freeIO[1]%>&1 %freeIO[2]%>&1
()%*



:enumIO
break %1>&%2 && (
    set /a "usedSlots+=1"
    call set "usedIO[%%usedSlots%%]=%2"
    exit /b
)
set /a "freeSlots+=1"
set "freeIO[%freeSlots%]=%2"
exit /b


REM *** Second thread to fetch and show the parameters
:StayAlive

:__WaitForParams
if %~z1 EQU 0 (
    goto :__WaitForParams
)
REM *** Show the result
findstr /n "^" %1
echo,

Re: foolproof counting of arguments

Posted: 02 Sep 2018 12:22
by sst
The solution I posted is not complete, it does not report the used and unused handles properly if handle 9 is already in use.
I was very tired, I completely forgot to take that into account, and honestly was not able to do that, my mind was down. I should never post anything when tired.

Here is the correct (I hope) and more a organized solution.

Code: Select all

@echo off
setlocal DisableDelayedExpansion
REM *** Thread redirector 
for /F "tokens=3 delims=:" %%F in ("%~0") do goto %%F

REM *** Clear params.tmp
break > params.tmp

start "" /b cmd /k @"%~d0\:StayAlive:\..\%~pnx0" "params.tmp"



setlocal EnableDelayedExpansion

:: Detect available handles without displaying error messages
:: Continue only if there is at least 3(2) unused handles. (One is used by stderr redirection)
call :enumIO
:: One of the handles was used by redirection of stderr
:: So we only need 2 additional free handles to continue.
if %freeSlots% LSS 2 (
    echo Not enough IO Slots.
    exit /b
)

:: First , do the permanent redirection of stderr. We don't have to know the backup handle.
:: The order of redirection is critical: stderr ---> freeSlots ---> usedSlots
set "stderr_permanent=break 2>nul"
for /L %%A in (%freeSlots%,-1,1) do set "stderr_permanent=!stderr_permanent! !freeIO[%%A]!>&2"
for /L %%A in (%usedSlots%,-1,1) do set "stderr_permanent=!stderr_permanent! !usedIO[%%A]!>&2"
%stderr_permanent%

echo One of the 'usedIO's is occupied by redirection of stderr
echo If there is more than one, then we don't which, And no need to know
echo Only CMD knows, This is the part that enables us to prevent the error message
echo This reminds me of Quantum uncertainty principle :)
echo,
set freeIO[
set usedIO[
set stderr_permanent


:: Next do a permanent redirection of stdout and stdin by known free handles.
(
    endlocal & endlocal %= To preserve prompt value after fatal error =%
    prompt #
    echo on >params.tmp 0>nul %freeIO[1]%>&1 %freeIO[2]%>&1
)
()%*

REM Unreachable

:enumIO
for /F "delims==" %%A in ('"(set freeIO[ & set usedIO[)2>nul"') do set "%%A=" // for displaying purposes

set /a "freeSlots=usedSlots=0"
2>nul (
    for /L %%A in (3,1,8) do call :nextIO 9 %%A
)
:: If non of the handles 4 to 8 are free then there are three possibilities for handle 9
:: 1. It is occupied before us, then we MAY have one free handle which is occupied by redirection of 9
:: 2. It is occupied by redirection of stderr, then we have no free handles
:: 3. It is free, then have only one free handle which is handle 9
:: either way we don't have the required free handles (2 handles)
if %freeSlots% NEQ 0 2>nul call :nextIO 1 9
if %freeSlots% NEQ 0 2>nul (
    set /a "highUsed=usedIO[%usedSlots%]"
    if "!highUsed!"=="9" (
        set /a "highFree=freeIO[%freeSlots%], highUsed=highFree+1"
        for /F "delims==" %%A in ('"(set freeIO[ & set usedIO[)2>nul"') do set "%%A=" // for displaying purposes
        set /a "freeSlots=usedSlots=0"
        for /L %%A in (3,1,!highFree!) do call :nextIO !highFree! %%A
        for /L %%A in (!highUsed!,1,9) do (
            set /a "usedSlots+=1"
            set "usedIO[!usedSlots!]=%%A"
        )
    )
)
exit /b
:nextIO
break %1>&%2 && (
    set /a "usedSlots+=1"
    set "usedIO[!usedSlots!]=%2"
    (call,)
) || (
    set /a "freeSlots+=1"
    set "freeIO[!freeSlots!]=%2"
)
exit /b


REM *** Second thread to fetch and show the parameters
:StayAlive

:__WaitForParams
if %~z1 EQU 0 (
    goto :__WaitForParams
)
REM *** Show the result
findstr /n "^" %1
echo,
Some test runs:
Q:\test>9>nul 8>nul 7>nul 4>nul getParams test
One of the 'usedIO's is occupied by redirection of stderr
If there is more than one, then we don't know which, And no need to know
Only CMD knows, This is the part that enables us to prevent the error message
This reminds me of Quantum uncertainty principle :)

freeIO[1]=5
freeIO[2]=6
usedIO[1]=3
usedIO[2]=4
usedIO[3]=7
usedIO[4]=8
usedIO[5]=9
stderr_permanent=break 2>nul 6>&2 5>&2 9>&2 8>&2 7>&2 4>&2 3>&2
1:
2:#()test
3:
4:#

Q:\test>7>nul 6>nul 3>nul getParams test
freeIO[1]=5
freeIO[2]=8
freeIO[3]=9
usedIO[1]=3
usedIO[2]=4
usedIO[3]=6
usedIO[4]=7
stderr_permanent=break 2>nul 9>&2 8>&2 5>&2 7>&2 6>&2 4>&2 3>&2
1:
2:#()test
3:
4:#

Q:\test>getParams test
freeIO[1]=4
freeIO[2]=5
freeIO[3]=6
freeIO[4]=7
freeIO[5]=8
freeIO[6]=9
usedIO[1]=3
stderr_permanent=break 2>nul 9>&2 8>&2 7>&2 6>&2 5>&2 4>&2 3>&2
1:
2:#()test
3:
4:#