Get batch file name from a subroutine - batch-file

In a subroutine, %0 expands to the subroutine name, not the script name. Is there a ligitimate way to still access the script name, or should I pass it as an argument?
#echo off
call :subroutine %~f0 my parameters
exit /b
:subroutine
shift
echo Script name is %0
echo Parameters: %1 %2
exit /b
I want the call statement to be just
call :subroutine my parameters

In a function you need to add at least one modifier to %~0.
call :test
exit /b
:test
echo 0 %0 - shows "test"
echo ~0 %~0 - shows "test"
echo ~f0 %~f0 - shows the batch name (full path)
exit /b

I believe %~nx0 will give you filename with extension and %~n0 will give you just the file name...

I prefer using this at the top of my scripts:
set "This=%~dpnx0"
This way you still keep the full path of the currently running script. If there's the need to get just the name of the script you can use a FOR /F loop to extract it:
set "This=%~dpnx0"
echo This=%This%
for /F %%I in ('echo.%This%') do set "Name=%%~nxI"
echo Name=!Name!

Related

Find and replace a string with special characters using a batch file

So I'm having some trouble getting this batch file to work correctly. I've tried several different ways to find and replace a string.
Problem: We have to update a database ever so often and in doing so, a file gets changed. I have to change the one file back to get things to work. This line:
<network ipAdress="172.24.55.32" networkPort="9100" />
gets changed to this line:
<file filename="fffff" />
and needs to be changed back to the first line with the ip address.
I have tried to use a find and replace subroutine and can get it to work on the first part of changing "file filename" to "network ipAdress" but the quotes in the second section keep the script from working correctly. This is the code as I have it.
#echo off
setlocal
call :FindReplace "file filename" "network ipAdress" BCPrint.XML
Timeout 2
call :FindReplace "fffff" "172.24.55.32" networkPort="9100" BCPrint.XML
exit /b
:FindReplace <findstr> <replstr> <file>
set tmp="%temp%\tmp.txt"
If not exist %temp%\_.vbs call :MakeReplace
for /f "tokens=*" %%a in ('dir "%3" /s /b /a-d /on') do (
for /f "usebackq" %%b in (`Findstr /mic:"%~1" "%%a"`) do (
echo(&Echo Replacing "%~1" with "%~2" in file %%~nxa
<%%a cscript //nologo %temp%\_.vbs "%~1" "%~2">%tmp%
if exist %tmp% move /Y %tmp% "%%~dpnxa">nul
)
)
del %temp%\_.vbs
exit /b
:MakeReplace
>%temp%\_.vbs echo with Wscript
>>%temp%\_.vbs echo set args=.arguments
>>%temp%\_.vbs echo .StdOut.Write _
>>%temp%\_.vbs echo Replace(.StdIn.ReadAll,args(0),args(1),1,-1,1)
>>%temp%\_.vbs echo end with
I have also tried setting the second part as a variable like:
set R2=172.24.55.32" networkPort="9100
call :FindReplace "file filename" "network ipAdress" BCPrint.XML
Timeout 2
call :FindReplace "fffff" "%R2%" BCPrint.XML
With escape characters as well:
set ^R2=172.24.55.32" networkPort="9100^
I am sure there are a couple of different ways that I have tried that I haven't listed like escape characters without using a variable. I've come to the end of what I can figure out.
Is there a way to find and replace a line that has special characters with a line that has special characters? Any advice on how to do either the entire line or that second section with the quotes and space would be great. Thanks for your time.
Download JREPL.BAT written by Dave Benham which is a batch file / JScript hybrid to run a regular expression replace on a file using JScript and store it in same directory as the batch file below.
#echo off
call "%~dp0jrepl.bat" "file filename=\x22fffff" "network ipAdress=\x22172.24.55.32\x22 networkPort=\x229100" /XSEQ /F BCPrint.XML /O -
\x22 is " specified in hexadecimal form as an argument string can't contain a double quote character.
Passing arguments containing quotation marks is very difficult, because there are several places where those characters are recognised, so you had to build up individual escape sequences (^) every time.
The following example script fails for every call attempt:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem // Define argument here:
set "_ARG=R2=172.24.55.32" networkPort="9100"
setlocal EnableDelayedExpansion
rem // Echo argument string to prove that variable is set:
echo(!_ARG!
echo/
rem // Call sub-routine with delayed expansion:
call :SUB "!_ARG!"
endlocal
rem // Call sub-routine with normal (immediate) expansion:
call :SUB "%_ARG%"
rem // Call sub-routine with double normal expansion (`call`):
call :SUB "%%_ARG%%"
endlocal
exit /B
:SUB
setlocal DisableDelayedExpansion
rem // Delayed expansion disabled during argument expansion:
set "ARG=%~1"
setlocal EnableDelayedExpansion
rem // Delayed expansion enabled for argument display (`echo`):
echo(!ARG!
endlocal
endlocal
exit /B
The easiest way to avoid such problems is to pass the variable name to the sub-routine rather than its value:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem // Define argument here:
set "_ARG=R2=172.24.55.32" networkPort="9100"
setlocal EnableDelayedExpansion
rem // Echo argument string to prove that variable is set:
echo(!_ARG!
echo/
endlocal
rem // Call sub-routine and pass variable name:
call :SUB _ARG
endlocal
exit /B
:SUB
setlocal EnableDelayedExpansion
set "#ARG=%~1"
rem /* Normal expansion is used to get the variable name;
rem delayed expansion is used to read its value: */
echo(!%#ARG%!
endlocal
exit /B
To update a string in a file that contains "quotes", the following solution will work.
Create a file named replace_quot.ps1 in the same directory as your bat file. Place the following 2 lines in this script.
param([string]$Name_of_File)
(Get-Content $Name_of_File) -replace '"', '"' | set-content $Name_of_File
In your bat file enter the below lines
set File_Name=File Name.txt
set "Find_String=<file filename="fffff" />"
set "Replace_String=<network ipAdress="172.24.55.32" networkPort="9100" />"
set "Find_String=%Find_String:"="%"
set "Replace_String=%Replace_String:"="%"
powershell -command "(Get-Content '%File_Name%') -replace '[\x22]', '"' | Set-Content '%File_Name%'"
powershell -command "(Get-Content '%File_Name%') -replace '%Find_String%', '%Replace_String%' | Set-Content '%File_Name%'"
Powershell.exe -executionpolicy remotesigned -File replace_quot.ps1 -Name_of_File "%File_Name%"
Update the File_Name value to reflect your file name. The bat script will replace the quotes with their hex value in both the strings and the file. Next it will find the string that needs to be replaced and restore it to the desired value. Lastly it will call the replace_quot.ps1 script to update the file's quote hex values to the actual quote character.

How to call a dynamic label in different .bat file

If I want to call :foo in foo.bat from bar.bat I do:
::foo.bat
echo.wont be executed
exit /b 1
:foo
echo foo from foo.bat
exit /b 0
and
::bar.bat
call :foo
exit /b %errorlevel%
:foo
foo.bat
echo.will also not be executed
But if I don't know the label name but get it passed as a parameter I'm stuck
::bar.bat
:: calling a dynamic label is no problem
call :%~1
exit /b %errorlevel%
::don't know how to "catch-all" or set context of "current-label"
:%~1
foo.bat
You can use a batch parser trick.
You don't need to do anything in foo.bat for this to work
::foo.bat
echo.wont be executed
exit /b 1
:func1
echo foo from foo.bat
exit /b 0
:unknown
echo Hello from %0 Func :unknown
exit /b
You only need to add any labels into bar.bat that you want to call in foo.bat
#echo off
::bar.bat
call :unknown
echo Back from Foo
call :func1
echo Back from Foo
exit /b %errorlevel%
:unknown
:func1
foo.bat
echo NEVER COMES BACK HERE
The trick is that after calling a label in bar.bat and then start foo.bat without calling it (only foo.bat), the foo.bat is loaded and the last called label is jumped to.
Labels are searched by the parser literally before resolving environment variables or argument references, so you cannot use such in labels.
However, hereby I want to provide an approach that allows to use dynamic labels, although I do not understand what is the purpose of that, so this is more kind of an academic answer...
The batch file parser of cmd does not cache a batch file, it reads and executes it line by line, or correctly spoken, it reads and executes each command line/block individually. So we can make use of that and let the batch file modify itself during its execution, given that the modified part lies beyond the currently executed code portion. The following script accomplishes that:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem // Check whether a label name has been delivered:
if "%~1"=="" (
echo ERROR: No label name specified! 1>&2
exit /B 1
)
rem /* Call sub-routine to replace the literal label string `:%~1`
rem within this batch file by the given dynamic label name: */
call :REPLACE_LINE "%~f0" ":%%%%~1" ":%~1" || (
rem /* In case the given label is `:REPLACE_TEXT`, display error
rem message, clean up temporary file and quit script: */
>&2 echo ERROR: Label ":%~1" is already defined!
2> nul del "%~f0.tmp"
exit /B 1
)
rem // Perform call of the sub-routine with the dynamic label name:
call :%~1
rem /* Call sub-routine to replace the given dynamic label name
rem within this batch file by the literal label string `:%~1`: */
call :REPLACE_LINE "%~f0" ":%~1" ":%%%%~1"
endlocal
exit /B
:REPLACE_LINE val_file_path val_line_LOLD val_line_LNEW
::This sub-routine searches a file for a certain line
::case-insensitively and replaces it by another line.
::ARGUMENTS:
:: val_file_path path to the file;
:: val_line_LOLD line to search for;
:: val_line_LNEW line to replace the found line;
setlocal DisableDelayedExpansion
rem // Store provided arguments:
set "FILE=%~1" & rem // (path of the file to replace lines)
set "LOLD=%~2" & rem // (line string to search for)
set "LNEW=%~3" & rem // (line string to replace the found line)
set "LLOC=%~0" & rem // (label of this sub-routine)
rem // Write output to temporary file:
> "%FILE%.tmp" (
rem /* Read the file line by line; precede each line by a
rem line number and `:`, so empty lines do not appear as
rem empty to `for /F`, as this would ignore them: */
for /F "delims=" %%L in ('findstr /N "^" "%FILE%"') do (
rem // Store current line with the line number prefix:
set "LINE=%%L"
setlocal EnableDelayedExpansion
rem // Check current line against search string:
if /I "!LINE:*:=!"=="!LOLD!" (
rem // Current line equals search string, so replace:
echo(!LNEW!
) else if /I not "!LNEW!"=="!LLOC!" (
rem // Current line is different, so keep it:
echo(!LINE:*:=!
) else (
rem /* Current line equals label of this sub-routine,
rem so terminate this and return with error: */
exit /B 1
)
endlocal
)
)
rem /* Searching and replacement finished, so move temporary file
rem onto original one, thus overwriting it: */
> nul move /Y "%FILE%.tmp" "%FILE%"
endlocal
exit /B
:%~1
::This is the sub-routine with the dynamic label.
::Note that it must be placed after all the other code!
echo Sub-routine.
exit /B
Basically, it first replaces the literal label string (line) :%~1 by the string provided as the first command line argument, then it calls that section by call :%~1, and finally, it restores the original literal label string. The replacement is managed by the sub-routine :REPLACE_LINE.
foo.bat (secondary):
#echo off
echo this is foo.bat
REM check, if the label is defined in this script:
findstr /xi "%~1" %~f0 >nul 2>&1 || goto :error
goto %~1
:foo
echo reached foo.bat, label :foo
exit /b 0
:error
echo wrong or missing label: "%~1"
exit /b 1
bar.bat (primary)
#echo off
echo this is bar.bat
call foo.bat :foo
echo back to bar.bat - %errorlevel%
call foo.bat :foe
echo back to bar.bat - %errorlevel%
call foo.bat
echo back to bar.bat - %errorlevel%
exit /b
As aschipfl already correctly stated you can't have a dynamic label, but you can dynamically call present labels, but you should check for the presence prior calling it like Stephan does but in the primary batch.
So this is a combination of Stephans and jebs batches.
:: bar.bat
#echo off
REM check, if the label is defined in this script:
If "%~1" neq "" findstr /xi "%~1" %~f0 >nul 2>&1||goto :error&&Call :%~1
call :Func1
echo Back from Foo
call :func2
echo Back from Foo
exit /b %errorlevel%
:error
echo wrong or missing label: "%~1"
exit /b 1
:func1
:func2
foo.bat
echo NEVER COMES BACK HERE
:: foo.bat
#Goto :Eof
:func1
echo reached foo.bat, label :func1
exit /b 0
:func2
echo reached foo.bat, label :func2
exit /b 0
Sample output:
> bar
reached foo.bat, label :func1
Back from Foo
reached foo.bat, label :func2
Back from Foo
> bar fx
wrong or missing label: "fx"

Batch-I want to set second part of variable

I have a problem.
set /p command=
if %command% == "display (i want the second part of the variable here)" echo (second part of the variable)
For example, i type:
display hello
I want it to simply:
echo hello
I want to use this for custom commands in my game.
Firstly, you can split the text on firstword|notfirstword with a for /f loop using "tokens=1*". See help for in a console window for full details.
Next, you could use attempt to call :label where :label is whatever the first word was. In essence, you're creating batch functions and letting the user choose which function is executed. If the function label doesn't exist, then errorlevel will be non-zero and you can handle appropriately using conditional execution. This makes it easy to expand your script without having to add an if /i statement for each choice or synonym you add. (It might be a good idea to hide the error message for attempting to call a non-existent label by redirecting 2>NUL.) Here's a full example:
#echo off & setlocal
:entry
set /P "command=Command? "
for /f "tokens=1*" %%I in ("%command%") do (
2>NUL call :%%I %%J || (
if errorlevel 1000 (exit /b 0) else call :unsupported %%I
)
goto :entry
)
:display
:echo
:say
:show
echo(%*
exit /b 0
:ping
ping %~1
exit /b 0
:exit
:quit
:bye
:die
echo OK, toodles.
exit /b 1000
:unsupported <command>
1>&2 echo %~1: unrecognized command
exit /b 0

Call a subroutine in a batch from another batch file

file1.bat:
#echo off
:Test
echo in file one
call file2.bat (Here i want to call only Demo routine in the file2.bat)
file2.bat:
:hello
echo in hello
:Demo
echo in Demo
From the batch file1 I want to make a call to a sub routine in the batch file2.
I tried for example call file2.bat:Demo but it didn't give the correct result.
How I can achieve this?
the file with subroutines must look like:
#echo off
call :%*
exit /b %errorlevel%
:hello
echo in hello
exit /b 0
:Demo
echo in Demo with argument %1
exit /b 0
then from the other file you can call it like
call file2.bat demo "arg-one"
You can write your functions file (in this sample it is library.cmd) as
#echo off
setlocal enableextensions
rem Not to be directly called
exit /b 9009
:test
echo test [%*]
goto :eof
:test2
echo test2 [%*]
goto :eof
:testErrorlevel
echo testErrorlevel
exit /b 1
And then the caller batch can be something like
#echo off
setlocal enableextensions disabledelayedexpansion
call :test arg1 arg2 arg3
call :test2 arg4 arg5 arg6
call :testErrorlevel && echo no errorlevel || echo errorlevel raised
goto :eof
:test
:test2
echo calling function %0
library.cmd %*
:testErrorlevel
echo calling function %0
library.cmd
In this case, the labels need to be defined with the same name in both files.
The direct invocation of the "library" batch file will replace the context of the call :label, and when the invoked batch is readed, a goto :label is internally executed and code continues inside the indicated label. When the called batch file ends, the context is released and the code after the call :label continues.
edited
As Jeb points in comments, there is a drawback in this method. The code running in the called batch file can not use %0 to retrieve the name of the function being called, it will return the name of the batch file. But if needed, the caller can do it as shown in the sample code.
edited 2016/12/27
Answering to dbenham, I have no way to know if it was a coding error or an intended feature, but this is how the process works
The lines in batch file are processed inside the inner BatLoop function when a batch "context" is created. This function receives, as one of its arguments, a pointer to the command that caused the "context" to be created.
Inside this function the commands in the batch file are iterated. The loop that iterates over the commands makes a test in each iteration: if extensions are enabled, it is the first line in the batch file and the arguments of the command that started the context starts with a colon (a label), a goto is generated to jump to the label.
Up to here, I have to suppose that this is the intended behaviour to handle the call :label syntax: create a new "context", load the file, jump to the label.
But the command argument received is never changed, a different variable is used to track the execution of the commands in the batch file. If a new batch file is loaded into / overwrites the current batch "context" (we have not used call command), after loading the new batch code, BatLoop resets the line count (we start at the first line of the loaded file) and, voila, the condition at the start of the loop (extensions enabled, first line, the colon) is true again (the pointed input command has not been changed) and a new goto is generated.
To comment a bit more, here is the code I came out reading the answer. It basically give you a 'utility' file where you can add your sub/fnc in a common library for code maintenance.
A) As a example, here is the 'utility_sub.cmd' file:
REM ==============================================
REM CALL THE SELECTED SUB
REM ==============================================
REM echo %~1
GOTO :%~1
REM ==============================================
REM ERROR MANAGEMENT
REM ==============================================
REM Ref : https://ss64.com/nt/exit.html
:RAISE_ERROR
EXIT /B 1
:RESET_ERROR
EXIT /B 0
REM Demo call
REM =========
REM CALL :RAISE_ERROR
REM echo RAISE_ERROR ERRORLEVEL = %ERRORLEVEL%
REM If %ERRORLEVEL% GTR 0 (
REM set msg="%tab%- Error detected ..."
REM CALL :SUB_STDOUT_MSG !msg!, 1
REM )
REM CALL :RESET_ERROR
REM echo RESET_ERROR ERRORLEVEL = %ERRORLEVEL%
REM ==============================================
REM SUB_STDOUT_MSG
REM ==============================================
:SUB_STDOUT_MSG
REM CALL :SUB_STDOUT_MSG "%param1%", %param2%, %param3%
REM Instead of this stdout sub, we can use Unix 'tee.exe'
REM but there is no 'line counter' feature like this sub
REM Call example :
REM EDI_Generate_Stat_Csv | tee c:\temp\voir.txt
REM Def :
REM Capture output from a program and also display the output to the screen, at the same time.
REM %~1 => Expand %1 removing any surrounding quotes (")
set msg=%~2
set sendtoLog=%3
set addCounter=%4
If !msg!==. (
REM Write empty line
echo!msg!
If !sendtoLog! EQU 1 (
echo!msg! >> %log_file%
)
) else (
REM (a) Write comment line (b) add counter if any
If !addCounter! EQU 1 (
set /a msgCounter+=1
set msg=!msgCounter! - !msg!
REM Pad counter left for single digit
If !msgCounter! LSS 10 (
set msg=0!msg!
)
)
REM Output to console
echo !msg!
REM Output to log
If !sendtoLog! EQU 1 (
echo !msg! >> %log_file%
)
)
EXIT /B
B) And here is how to call the 'SUB_STDOUT_MSG' in you 'main-logic' command file:
REM ... some other code here
REM ==============================================
REM PROGRAM END
REM ==============================================
set msg=.
CALL :SUB_STDOUT_MSG !msg!, 1
set msg="My programA - End"
CALL :SUB_STDOUT_MSG !msg!, 1
set msg="%date:~0,4%-%date:~5,2%-%date:~8,2% %time:~0,2%:%time:~3,2%:%time:~6,2%"
CALL :SUB_STDOUT_MSG !msg!, 1
set msg="+++++++++++++++"
CALL :SUB_STDOUT_MSG !msg!, 1
timeout 2 > Nul
REM Skip all SUB ROUTINE
GOTO :EOF
REM ==============================================
REM CALL SUB ROUTINE
REM ==============================================
:SUB_STDOUT_MSG
REM echo calling sub %0
CALL "C:\Utilitaires\Financement\Utility_Sub.cmd" SUB_STDOUT_MSG %*
EXIT /B
:EOF
What about providing the target label as the first agrument of the called script? You needed to modify the called dscript then though.
file1.bat (main):
#echo off
echo/
echo File "%~0": call "file2.bat" [no arguments]
call "file2.bat"
echo/
echo File "%~0": call "file2.bat" :DEMO
call "file2.bat" :DEMO
echo/
echo File "%~0": call "file2.bat" :DEMO A B C
call "file2.bat" :DEMO A B C
file2.bat (sub):
#echo off
set "ARG1=%~1" & if not defined ARG1 goto :TEST
if "%ARG1:~,1%"==":" goto %ARG1%
:TEST
echo File "%~nx0", :TEST; arguments: %*
goto :EOF
:DEMO
echo File "%~nx0", :DEMO; arguments: %*
echo before `shift /1`:
echo "%%~0" refers to "%~0"
echo "%%~1" refers to "%~1"
shift /1
echo after `shift /1`:
echo "%%~0" refers to "%~0"
echo "%%~1" refers to "%~1"
goto :EOF
Output:
>>> file1.bat
File "file1.bat": call "file2.bat" [no arguments]
File "file2.bat", :TEST; arguments:
File "file1.bat": call "file2.bat" :DEMO
File "file2.bat", :DEMO; arguments: :DEMO
before `shift /1`:
"%~0" refers to "file2.bat"
"%~1" refers to ":DEMO"
after `shift /1`:
"%~0" refers to "file2.bat"
"%~1" refers to ""
File "file1.bat": call "file2.bat" :DEMO A B C
File "file2.bat", :DEMO; arguments: :DEMO A B C
before `shift /1`:
"%~0" refers to "file2.bat"
"%~1" refers to ":DEMO"
after `shift /1`:
"%~0" refers to "file2.bat"
"%~1" refers to "A"

How to use CALL function in DOS 6.22?

I have written a batch file that runs fine under Windows Command Prompt, but I would like to be able to run it after POST in DOS. I have copied my code to AUTOEXEC.BAT file which gets executed automatically; however it comes up with syntax errors once it reaches the call command and the rest.
echo. This script is counting the # of POSTs.
echo. The POST # value is saved in TEST.txt.
echo.
call:myPOSTTest
for /f "tokens=* delims=" %%x in (A:\TEST.txt) do echo POST# %%x
echo. &pause&goto:eof
::--------------------------------------------------------
::-- Function section starts below here
::--------------------------------------------------------
:myPOSTTest - here starts my function identified by its label
set var=0
if EXIST A:\TEST.txt (
for /f %%x in (A:\TEST.txt) do (set /a var=%%x+1)
)
echo %var% >> A:\TEST.txt
goto END
:END
Thank You
See below for comments:
echo. This script is counting the # of POSTs.
echo. The POST # value is saved in TEST.txt.
echo.
call:myPOSTTest
MSDOS doesn't support the call :label syntax
for /f "tokens=* delims=" %%x in (A:\TEST.txt) do echo POST# %%x
MSDOS doesn't support the extended for commands
echo. &pause&goto:eof
MSDOS doesn't support the & command separator or the goto :eof link
::--------------------------------------------------------
::-- Function section starts below here
::--------------------------------------------------------
:myPOSTTest - here starts my function identified by its label
set var=0
if EXIST A:\TEST.txt (
for /f %%x in (A:\TEST.txt) do (set /a var=%%x+1)
)
MSDOS doesn't support the compound expressions in parentheses or the set /a enhancement
echo %var% >> A:\TEST.txt
goto END
:END
A little bit late, but ...
The call :function syntax can be substituted by an additional file for each function or by trampoline code on the batch file entry.
#echo off
for %%P in (/%1.) do if %%P==: goto %1
echo Program started
call %0 :func1
call %0 :func2 With :args
goto :eof
:func1
echo This is func1 with args: %1, %2, %3
goto :eof
:func2
echo This is func2 with args: %1, %2, %3
goto :eof
:eof
For the arithmetic part in set /a var=%%x+1 you can use an inc.bat or add.bat.
Even reading and processing lines from a text file is possible with old DOS.

Resources