Heredoc (Multilined Input) in Win BAT script - windows

Is there any way to achieve this without the need of external resources as Bash does?
I'd like to prompt user for input and copy the input into a variable in case it is multilined as well (for instance if he or she copy-pastes a text onto the prompt)
Perhaps using Powershell -command?

As Aacini mentioned, it's not possible to use a single set /p.
You could use a loop of set /p, but you need a type of end marker to leave the loop.
Or you could use copy con input.tmp, but then you have to finish the input with CTRL-Z.
A sample with sep /p stopping at the first empty line.
#echo off
setlocal EnableDelayedExpansion
SET LF=^
REM ** Two empty lines are required
call :input
echo !input!
exit /b
:input
set "input="
:inputLoop
set "line="
set /p "line=Enter text, stop with an empty line :"
if defined line (
set "input=!input!!line!!LF!"
goto :inputLoop
)
exit /b

Related

Reading lines from a txt file into variables in batch

I am trying to figure out how to read IP addresses from a file named "IPList.txt) into individual variables in a batch script. Here's what I have so far.
:DEFINITIONS
set LOGFILE=IPScript.log
set IPLIST=C:\IPLIST.txt
echo Script Started >> %LOGFILE%
goto SetIP
:SetIP
for /f "tokens=*" %%a in (%IPLIST%) do (
set FirstIP=%%a
)
echo The first IP is %FirstIP% >> %LOGFILE%
exit
The output I'm getting in "IPscript.log" is "The First IP is: " with no IP listed, just a space. Also, is there a way for me to set multiple IPs like this, in just one for loop?
Here's a quick example to assist you:
#Echo Off
SetLocal EnableExtensions DisableDelayedExpansion
:DEFINE_LOCAL_VARIABLES
Set "IPLIST=C:\IPLIST.txt"
Set "LOGFILE=IPScript.log"
:CHECK_SOURCE_EXISTS
For %%G In ("%IPLIST%") Do If "%%~aG" Lss "-" (
Echo The file %IPLIST% does not exist.
Echo Press any key to end this script.
Pause 1> NUL
GoTo :EOF
) Else If "%%~aG" GEq "d" (
Echo Expected a file, but %IPLIST% is a directory.
Echo Press any key to end this script.
Pause 1> NUL
GoTo :EOF
)
:UNDEFINE_LOCAL_VARIABLES
For /F "Delims==" %%G In ('"(Set IP[) 2> NUL"') Do Set "%%G="
:START_MAIN
Set "i=1000"
(
Echo Script Started
For /F UseBackQ^ Delims^=^ EOL^= %%G In ("%IPLIST%") Do (
Set /A i += 1
SetLocal EnableDelayedExpansion
For %%H In ("!i:~-3!") Do (
EndLocal
Set "IP[%%~H]=%%G"
Echo IP[%%~H] is %%G
)
)
) 1> "%LOGFILE%"
:CHECK_IP_VARIABLES_EXIST
If Not Defined IP[001] (
Echo %IPLIST% had no readable file content.
Echo Press any key to end this script.
Pause 1> NUL
GoTo :EOF
)
:VIEW_IP_VARIABLES
Set IP[
Pause & GoTo :EOF
If you have an existing %LOGFILE%, and you intend to append to it, (as opposed to overwrite/create one), change 1> "%LOGFILE%" to 1>> "%LOGFILE%".
If you didn't really need %LOGFILE%, e.g. it was used by you just for testing, it would look a little more like this:
#Echo Off
SetLocal EnableExtensions DisableDelayedExpansion
:DEFINE_LOCAL_VARIABLES
Set "IPLIST=C:\IPLIST.txt"
:CHECK_SOURCE_EXISTS
For %%G In ("%IPLIST%") Do If "%%~aG" Lss "-" (
Echo The file %IPLIST% does not exist.
Echo Press any key to end this script.
Pause 1> NUL
GoTo :EOF
) Else If "%%~aG" GEq "d" (
Echo Expected a file, but %IPLIST% is a directory.
Echo Press any key to end this script.
Pause 1> NUL
GoTo :EOF
)
:UNDEFINE_LOCAL_VARIABLES
For /F "Delims==" %%G In ('"(Set IP[) 2> NUL"') Do Set "%%G="
:START_MAIN
Set "i=1000"
Echo Script Started
For /F UseBackQ^ Delims^=^ EOL^= %%G In ("%IPLIST%") Do (
Set /A i += 1
SetLocal EnableDelayedExpansion
For %%H In ("!i:~-3!") Do (
EndLocal
Set "IP[%%~H]=%%G"
)
)
:CHECK_IP_VARIABLES_EXIST
If Not Defined IP[001] (
Echo %IPLIST% had no readable file content.
Echo Press any key to end this script.
Pause 1> NUL
GoTo :EOF
)
:VIEW_IP_VARIABLES
Set IP[
Pause & GoTo :EOF
The last line in both examples is for display purposes. If you're testing/running this script from within cmd.exe, you may omit it.
FOR /f "tokens=1*delims=:" %%a IN ('findstr /n /r ".*" "%filename1%"') DO set "IP%%a=%%b"
)
set IP
findstr reads the file in filename1 and produces a list of the format n:content of line n.
The for /f reads this list, and partitions it using 2 tokens - %%a gets the first token (1) and %%b the remainder of the line (*) using : as a delimiter.
So simply set the IP variables from there.
set ip displays all variables that start ip
Probability is that your file contains empty line(s) after the last IP. Your original code would have reported the LAST IP, not the FIRST as the value in firstip is overwritten on each iteration, so it would be cleared by being set to nothing when the empty lines are read.
The solution above would simply execute (eg) set "IP6=" under these circumstances, clearing the variable.
You could have obtained the first IP by using
if not defined firstip set "FirstIP=%%a"
I'm assuming a clean environment here - that is, that each batch you run includes a setlocal after the #echo off (which restores the initial environment when the batch finishes) and the variables used are known-empty.
Bonus:
changing the set command to
set "IP%%a=%%b"&if "%%b" neq "" set "ipmax=%%a"
would set ipmax to the number of the last non-empty line, as %%b is empty for an empty line.
The batch file could have following command lines:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "LOGFILE=%~dp0IPScript.log"
set "IPLIST=%~dp0IPLIST.txt"
set "AddressCount=0"
echo Script started>"%LOGFILE%"
for /F "delims==" %%I in ('set IP_Address_ 2^>nul') do set "%%I="
if exist "%IPLIST%" for /F "useback delims=" %%I in ("%IPLIST%") do (
set /A AddressCount+=1
call set "IP_Address_%%AddressCount%%=%%I"
)
if not %AddressCount% == 0 (
if %AddressCount% == 1 (
echo The IP address is:
) else echo The IP addresses are:
echo/
set IP_Address_
) >>"%LOGFILE%"
endlocal
The batch file first two command line define the execution environment which means:
Disable command echo mode.
Push current command extension state on stack and enable command extensions.
Push current delayed expansion state on stack and disable delayed environment variable expansion.
Push path of current directory on stack.
Push pointer to current list of environment variables on stack and create a copy of the entire current environment variables list to use next.
The third and fourth line define two environment variables with the name of the log file and the name of the IP address list file with full qualified file name. The file path of both files is defined as path of the directory containing the batch file referenced with %~dp0. This path always ends with \ and for that reason no additional backslash is needed on concatenating this path with the two file names.
The fifth line define the environment variable AddressCount with value 0.
The sixth line creates the log file in current directory with overwriting an already existing log file. There is no space left to redirection operator > as this space would be output by command ECHO and therefore written as trailing space also into the log file.
The first FOR command with option /F starts in background with %ComSpec% /c one more command process with the command line between ' appended as additional arguments. So executed is in background with Windows installed into C:\Windows:
C:\Windows\System32\cmd.exe /c set IP_Address_ 2>nul
Windows creates a copy of current list of environment variables for the command process started in background. The background command process runs command SET to output all environment variables with name, an equal sign and the string value assigned to the variable line by line of which name starts with IP_Address_. This output to handle STDOUT of background command process is captured by FOR respectively the command process which is processing the batch file. The error message output by SET on no environment variable define with a name starting with IP_Address_ is redirected from handle STDERR to device NUL to suppress this error message.
Read the Microsoft documentation about Using command redirection operators for an explanation of 2>nul. The redirection operator > must be escaped with caret character ^ on FOR command line to be interpreted as literal character when Windows command interpreter processes this command line before executing command FOR which executes the embedded dir command line with using a separate command process started in background.
FOR processes the captured output line by line after started background command process closed itself after execution of command SET. Empty lines are always ignored by FOR which can be ignored as there are no empty lines output by SET.
FOR would split up by default the current line into substrings using normal space and horizontal tab as delimiters. This default line splitting behavior is not wanted here. The option delims== defines the equal sign as string delimiter to split the line on = which is the character between variable name and variable value.
FOR would next ignore the line if the first substring would start with a semicolon which is the default end of line character. The command SET outputs only lines starting with IP_Address_ and for that reason the default eol=; can be kept in this case.
FOR assigns just the first substring to the specified loop variable I as tokens=1 is the default. That is exactly the wanted behavior in this case.
So FOR assigns one environment variable name starting with IP_Address_ to loop variable I and runs next the command SET to delete this environment variable in current list of environment variables of command process processing the batch file.
In other words the first FOR is for deletion of all environment variables of which name starts with IP_Address_ defined by chance outside the batch file.
The next line first checks if the file with the list of environment variables exists at all in directory of the batch file. In this case once again FOR is used to process lines, but this time read line by line from the specified list file instead of captured output of a background command process. The usage of " instead of ' with the option usebackq makes the difference.
There is used the option delims= to define an empty list of delimiters resulting in getting each non-empty line not starting with ; assigned completely to the specified loop variable I.
For each string assigned to loop variable I the current value of environment variable AddressCount is incremented by one using an arithmetic expression evaluated by command SET.
This value is used on next command line to define an environment variable of which name starts with IP_Address_ and has appended the current address count value with line read from file assigned to the environment variable.
There is usually used delayed expansion for such tasks on which the second command line in command block of second FOR loop would be:
set "IP_Address_!AddressCount!=%%I"
But the code above uses the alternative method with command call to parse set "IP_Address_%%AddressCount%%=%%I" a second time which was already modified to set "IP_Address_%AddressCount%=%I" before the IF condition left to FOR was executed at all.
The next IF condition checks if any line was read from the list file with the IP addresses. In this case first an information line is output depending on having read exactly one line from the file or more than one line. Then an empty line is output and last all environment variables of which name starts with IP_Address_ with = and the line (IP address) assigned to the environment variable. All this output is appended to the log file.
The last command restores previous execution environment which means:
Discard the current list of environment variables and pop from stack the pointer to initial list of environment variables resulting in restoring the initial list of environment variables. In other words all environment variables defined or modified by the batch file after command SETLOCAL in second command line are lost forever.
Pop path of current directory from stack and make this directory again the current directory. The current directory between setlocal and endlocal was not changed by the code between and so this does not matter here.
Pop delayed expansion state from stack and enable or disable delayed environment variable expansion accordingly to restore initial delayed expansion behavior.
Pop current command extension state from stack and enable or disable command extensions accordingly to restore initial command extension behavior.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
call /?
echo /?
endlocal /?
for /?
if /?
set /?
setlocal /?
See also:
Variables are not behaving as expected
How does the Windows Command Interpreter (CMD.EXE) parse scripts?
Microsoft's documentation for the Windows Commands
SS64.com - A-Z index of Windows CMD commands

Batch file seems to be setting a variable to 2 for no reason

I'm making a simple batch script to figure out arrays in batch script.
The code:
#echo off
setlocal EnableDelayedExpansion
set inputCount=0
set outputCount=0
:input
cls
set /p !number%inputCount%!=Input %inputCount%:
set /a inputCount=%inputCount%+1
if %inputCount% geq 3 goto output
goto input
:output
cls
echo !number%outputCount%!
set /a outputCount=%outputCount%+1
if %outputCount% geq 3 goto exit
goto output
:exit
pause
echo exit
On line 4, I set outputCount to 0, I then don't change outputCount until line 16 where I add 1 to it.
I expected the output of line 16 to be outputCount=0+1=1 therefore making outputCount=1. However, when I run the code with echo on to see exactly what it's doing, the output for line 16 is outputCount=2+1=3 setting outputCount to 3.
It seems that the program is setting outputCount to 2 instead of 0 at some point before line 16 but I can't see why.
First, take a look on Debugging a batch file as this is a lesson you need to learn on coding a batch file.
Second, read the answer on Why is no string output with 'echo %var%' after using 'set var = text' on command line? which offers additional information to the help output on running in a command prompt window set /?.
It looks like you want to define the environment variables number0, number1 and number2 with a string assigned to them by user input.
The command to use to prompt a user for a string is either
set /P "variable=prompt text: "
or
set /P variable="prompt text: "
The first variant is in general recommended although most often not used by batch file coding newbies because of not knowing how to use the double quotes right on assigning a string to an environment variable. The second variant is specific for set /P also possible and in some very rare cases really needed, but in my point of view should be avoided to use because of the double quotes are interpreted different on using set without /P.
So let us look on your code with commenting out with rem four lines, appending one more line with set at end of the batch file and run that batch file from within a command prompt window:
rem #echo off
setlocal EnableDelayedExpansion
set inputCount=0
set outputCount=0
:input
rem cls
set /p !number%inputCount%!=Input %inputCount%:
set /a inputCount=%inputCount%+1
if %inputCount% geq 3 goto output
goto input
:output
rem cls
echo !number%outputCount%!
set /a outputCount=%outputCount%+1
if %outputCount% geq 3 goto exit
goto output
:exit
rem pause
echo exit
set number
Output is at end just a line with NUMBER_OF_PROCESSORS=2. There are no environment variables number0, number1, number2 which would be also output by set number. And the command line echo !number%outputCount%! in file results three times in the information that ECHO is OFF.
The reason can be seen on looking on the output command lines really executed after preprocessing each line by Windows command interpreter.
set /p !number%inputCount%!=Input %inputCount%:
The string entered by the user, if not just RETURN or ENTER was hit by the user on prompt, should be assigned to the environment variables of which name are stored in the environment variables number0, number1 and number2. But the environment variables number0, number1 and number2 are never defined by your batch file as your intention is to store the input strings into the variables with name number0, number1 and number2. So this command line is finally on execution:
set /p =Input 0:
set /p =Input 1:
set /p =Input 2:
Those command lines would result in an exit of batch processing because of a syntax error, but this does not occur here because of usage of delayed expansion as it can be seen in the console window.
The solution is a batch code as follows:
#echo off
setlocal EnableExtensions EnableDelayedExpansion
set "Index=0"
:Input
cls
set /A Index+=1
set /P "Number%Index%=Input %Index%: "
if not %Index% == 3 goto Input
cls
set "Index=0"
:Output
set /A Index+=1
if defined Number%Index% echo Number%Index%=!Number%Index%!
if not %Index% == 3 goto Output
endlocal
The output of this batch file on entering on first prompt Hello!, on second prompt nothing and on third prompt Bye! is:
Number1=Hello!
Number3=Bye!
Okay, we have not entered a number as we have the freedom to type anything from nothing to bad strings like Double quote " or | or < or > are bad inputs on user prompt on being prompted for an input. But the batch file works as expected now.
Further please note that the string after set /A is an arithmetic expression which is parsed completely different to any other string on a command line. For examples the current values of environment variables can be referenced by using just the variable name without surrounding percent signs or exclamation marks. That make it possible to use variables in arithmetic expression within an IF or FOR command block without usage of delayed expansion. The help output on running set /? in a command prompt window explains this different parsing behavior quite good as well as which operators can be used like += in the arithmetic expression.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
cls /?
echo /?
endlocal /?
goto /?
if /?
rem /?
set /?
setlocal /?
Some more hints:
Don't use just exit in a batch file, use exit /B or goto :EOF, see Where does GOTO :EOF return to?
After verification that the user entered anything at all and the entered string is really a number (decimal, octal or hexadecimal), make sure to process the number right according to your and the users' expectations. What I mean here is demonstrated with:
#echo off
set "Number=020"
set /A Number+=1
echo Result=%Number%
pause
What do you expect as result, 21 or the real output result 17?
A number string starting with 0 is interpreted as octal number. A number string starting with 0x or 0X is interpreted as hexadecimal number. Change 020 to 008 and the result is 1. Why? 008 is invalid for an octal number and therefore replaced by 0 which is incremented by one.

How to get a specific line from command output in batch script into a variable?

I'd like to get a changelist description from perforce, which involves calling a p4 describe -s , so the ouput would be as below. Is there a way to get (trimmed characters from the third line) from the output just using windows batch syntax?
Change 6582 by username on 2016/12/06 00:35:41
MyChangeDescription
Affected files ...
... //depot/foo.txt#7 edit
... //depot/foo2.txt#6 edit
Give this a shot:
p4 -Ztag -F %Description% change -o 6582
#ECHO OFF
SETLOCAL
SET "sourcedir=U:\sourcedir"
SET "filename1=%sourcedir%\q40986156.txt"
FOR /f "usebackqskip=2tokens=*" %%a IN ("%filename1%") DO (
SET "desc=%%a"
GOTO show
)
:show
ECHO "%desc%"
GOTO :EOF
You would need to change the setting of sourcedir to suit your circumstances.
I used a file named q40986156.txt containing your data for my testing.
This uses a file as input. Since I don't have access to perforce, I can't test it but
#ECHO OFF
SETLOCAL
FOR /f "skip=2tokens=*" %%a IN ('p4 describe -s') DO (
SET "desc=%%a"
GOTO show
)
:show
ECHO "%desc%"
GOTO :EOF
should be equivalent.
Simply, read the output of the command, skip the first 2 lines, tokenise the entire line, skipping leading spaces. Assign the string found to a variable and immediately exit the loop.

How do I detect if script was CALL'ed or invoked directly, in Windows CMD.EXE shell?

I need to distinguish these two situations inside script.cmd:
C:\> call script.cmd
C:\> script.cmd
How can I determine if my script.cmd was invoked directly, or invoked in the context of using a CALL?
If it matters, this is on Windows 7.
#echo off
set invoked=0
rem ---magic goes here---
if %invoked%==0 echo Script invoked directly.
if %invoked%==1 echo Script invoked by a CALL.
Anyone know the "magic goes here" which would detect having been CALL'ed and set invoked=1?
At this moment, I see no way to detect it, but as a workaround you can always force the use of the sentinel.
#echo off
setlocal enableextensions
rem If "flag" is not present, use CALL command
if not "%~1"=="_flag_" goto :useCall
rem Discard "flag"
shift /1
rem Here the main code
set /a "randomExitCode=%random% %% 2"
echo [%~1] exit with code %randomExitCode%
exit /b %randomExitCode%
goto :eof
rem Retrieve a correct full reference to the current batch file
:getBatchReference returnVar
set "%~1=%~f0" & goto :eof
rem Execute
:useCall
setlocal enableextensions disabledelayedexpansion
call :getBatchReference _f0
endlocal & call "%_f0%" _flag_ %*
This will allow you to use the indicated syntax
script.cmd first && script.cmd second && script.cmd third
The posted code ends the script with a random exit code for testing. Execution will continue when the exit code is 0
NOTE: For it to work, at least in XP, it seems the call to the batch file MUST be the last code in the batch file
Check if the script's path is in the CMDCMDLINE variable. If not, then it was probably called.
In this example I use %CMDCMDLINE:"=/% to turn the quotes into forward-slashes (the FIND command can't search for quotes) and I echo it with <NUL SET/P="" so that certain characters in the file path (like ampersands) don't break the script.
<NUL SET/P="%CMDCMDLINE:"=/%" | FIND "/%~0/">NUL || (
REM Commands to perform if script was called
GOTO:EOF
)
::AND/OR
<NUL SET/P="%CMDCMDLINE:"=/%" | FIND "/%~0/">NUL && (
REM Commands to perform if script was NOT called
GOTO:EOF
)

Writing a script that can be both piped to and given direct input

This is a follow-on question from here, posted at the suggestion of the person who originally answered it.
I'm trying to create a script that will both take piped input and also direct input from the command line. I wrote two scripts; the first is called create and the other is called edit. I give the code below.
#echo off
REM create
if -%1-==-- (
REM Piped
set /p name=
copy nul %name% > nul
echo %name%
) else (
REM Not piped
copy nul %1 > nul
)
#echo off
REM edit
if -%1-==-- (
REM Piped
set /p file=
start notepad++.exe %file%
) else (
REM Not piped
start notepad++.exe %1
)
I have added remarks to clarify which is which, and what my intent was. Now, the normal input (for example, if I just type create foo.txt into the command prompt) works fine, but the moment I try piping (for example, create foo.txt | edit) I get strange behaviour, namely, the edit script tries to open a completely different file to foo.txt in this example! What am I missing or doing wrong?
In batch files, when execution reaches a line, it is parsed and then executed. At parse time, all variable reads inside the line are replaced with their corresponding value.
This applies to lines and blocks, where a block are all the lines enclosed in parenthesis. Blocks are readed, parsed, and get variables reads replaced with variables values before executing any line inside the block.
What you are seeing in your code is that the variable %file% gets its value assigned inside a block, and then, inside the same block, you try to read the variable value. But as all variable reads has been replaced with the value they have before entering the block, you can't get the changed value.
To handle it, delayed expansion is needed. When delayed expansion is active, you can indicate to cmd that some variable reads should be delayed to the moment the line is executed. Your code should be something like
#echo off
setlocal enabledelayedexpansion
REM create
if -%1-==-- (
REM Piped
set /p name=
copy nul !name! > nul
echo !name!
) else (
REM Not piped
copy nul %1 > nul
)
#echo off
setlocal enabledelayedexpansion
REM edit
if -%1-==-- (
REM Piped
set /p file=
start notepad++.exe !file!
) else (
REM Not piped
start notepad++.exe %1
)
variables that need delayed read are accessed using !var! sintax.

Resources