Batch Script Chokes on ! Exclamation Point in Filename - batch-file

The Batch file below fails to encode any .mkv file with an exclamation point (!) in the filename.
Not sure what the problem is.
#echo off
SETLOCAL EnableDelayedExpansion
rem <BUG>If the input .mkv filename contains a ! (exclamation mark), the script will not encode the file and continue to the next file
echo Handbrake_Encode_MKV_offPeak-beta.bat
for /r %%a in (*.mkv) do (
rem call function to pause script until electricity rates are lowest
call :sleepUtilOffPeak
rem strip parent directory from the path (set str = filename.ext)
rem set str=%%~nxa
rem strip extension from filename
rem set str=!str:~0,-4!
rem get filename and drive+path
set filename=%%~na
set drive_and_path=%%~dpa
rem echo Calling Handbrake to encode^: !str!.mp4
echo Calling Handbrake to encode^: !drive_and_path!!filename!.mp4
"C:\Scripts\HandBrakeCLI.exe" -v 0 --preset-import-file "C:\Scripts\Fast 1080p30-subs.json" -Z "Fast 1080p30-subs" -i "%%a" -o "!drive_and_path!!filename!.mp4"
)
echo encoding complete. Exiting...
exit /B 0
The Handbrake log complains about a "Missing output file name"
Handbrake_Encode_MKV_offPeak-beta.bat
Day of week: 0
Hour: 14
Minute: 43
Today is a weekend
14 is not less than off-peak end : 14
14 is between 14 and 24
Going to sleep for 33420 seconds
All done sleeping. Time to work...
Calling Handbrake to encode: E:\Mamasota\Unsorted\dvds\_Encode\mkv\Scooby-Doo and Kiss Rock and Roll Mystery (2015).mp4
[00:00:13] Compile-time hardening features are enabled
[00:00:13] qsv: not available on this system
[00:00:13] vcn: not available on this system
Cannot load nvEncodeAPI64.dll
[00:00:14] hb_init: starting libhb thread
[00:00:14] thread 1 started ("libhb")
Missing output file name. Run C:\Scripts\HandBrakeCLI.exe --help for syntax.
HandBrake has exited.
To see what is being passed as input to Handbrake when the filename (Scooby-Doo!) includes an exclamation point:
echo "%%a"
The output was:
Scooby-Doodrive_and_path
Using
setlocal DisableDelayedExpansion
before calling Handbrake corrects the input file name but the output filename is not expanded?
Calling Handbrake to encode: "Scooby-Doo! & Batman The Brave and the Bold (2018).mkv" to !drive_and_path!!filename!.mp4
I also tried reading similar threads but I'm not sophisticated enough to understand what I'm doing wrong.

tl;dr: Remove the variables entirely and just use %%~dpna.mp4 as the output filename.
When you enable delayed expansion, ! becomes a character that has special meaning - it indicates to the interpreter that the variable should have its value expanded when the line is executed rather than when the line is initially read in. If you're simply printing a ! with delayed expansion enabled, you can escape it with a ^ so that it displays correctly. However, since you're iterating over filenames, you have no way of escaping !s in filenames so the only way to correctly process them is to disable delayed expansion.
Unfortunately, since you're trying to set and use the filename and drive_and_path variables inside of a set of parentheses, you have to use delayed expansion if you want those variables to have usable values inside of the loop. This is because a set of parentheses is read in and treated as a single command in batch. Since you aren't doing any sort of string manipulation with those variables like getting substrings or doing substitutions, you can get around this limitation by removing the variables entirely and simply using %%~dpna wherever in the code you want to use !drive_and_path!!filename!.

Related

Batch - Remove Range/specific words

In perforce I print the list of shelved/changes to the file with the following batch script :
for %%A IN (%ShelvedCHL%) DO (
echo Change List: %%A
p4 -p %PPort% -c %PClient% unshelve -s %%A && if NOT %ERRORLEVEL%==0 exit -1
)>> list.txt
Here is my list.txt
Change List: 24536
//GSA/TOC.h#1 - unshelved, opened for edit
... /GSA/TOC.h - also opened by test#dev
... /GSA/TOC.h - also opened by test#dev
//odata/oenums.h#6 - unshelved, opened for edit
... //odata/oenums.h#6 - also opened by test#dev
I want to have the following output, basically remove sentence after - with dash as well : or even any perforce command to have less information and just list of files :
//GSA/TOC.h#1
... /GSA/TOC.h
... /GSA/TOC.h
//odata/oenums.h#6
... //odata/oenums.h#6
I would appreciate any help, thanks in advance!
You can do this in p4 natively without doing hardly any scripting at all. Check out this blog post:
https://www.perforce.com/blog/fun-formatting
You can see the fields in a Perforce message by running it with the -e global opt*, like this:
p4 -e files ...
Then you can use any or all of those fields when reformatting the output, like this:
p4 -F "just the filename please: %depotFile%" files ...
*see also -Ztag which gives you an alternate dictionary of output fields
At first, let us take a look at your code:
I do not know the program p4, but I assume it is setting the ErrorLevel. So since this value is updated in the same block of code as you want to read it, you need to use delayed expansion, so place setlocal EnableDelayedExpansion on top of your script and use !ErrorLevel! instead of %ErrorLevel%. Another way is to replace if not !ErrorLevel!==0 by ifnot ErrorLevel 1, meaning if ErrorLevel is not greater than and not equal to 1, or expressed in a simpler way, if ErrorLevel is less than 1, but this works only if the program does not set a negative value.
Even if you corrected the ErrorLevel issue, the if query would never eb executed because of the conditional command concatenation operator %%, because this lets the following command only execute in case the preceding one succeeded, meaning that its exit code1 equals zero. Therefore to execute the if statement, use the unconditional operator &. Anyway, there is also another conditional operator ||, which lets the following command only execute in case the exit code is a non-zero value; this one could replace your if condition completely.
The exit command does not only quit the batch file, it also terminates the command prompt (cmd) instance which the batch script ran in. To quit the batch file only use exit /B instead.
You are setting the ErrorLevel to -1 by exit -1. You can do this, of course, but usually negative values are avoided; hence let me suggest a positive value like 1 (by exit /B 1).
You are opening and closing the file list.txt for every single iteration of the for loop. This reduces overall performance. Furthermore, if list.txt already exists, the data becomes appended; if you do not want that you need to place del "list.txt" 2> nul before the for loop to initially delete the file. Anyway, to write the entire file at once, put another pair of parentheses around the for loop. You can then chose whether to append to an already existing file using the redirection operator >>, or to overwrite it using operator > (without any need to delete it first).
All this results in the following improved script:
(for %%A in (%ShelvedCHL%) do (
echo Change List: %%A
p4 -p %PPort% -c %PClient% unshelve -s %%A || exit /B 1
)) > "list.txt"
Depending on what %ShelvedCHL% contains (it seems to be 24536 in your sample data, so not a file path/name/mask), the for loop might even be superfluous, although I cannot know at this point...
Anyway, all of the above does still not yet account for removal of the partial string beginning with SPACE + - + SPACE, so let us implement this now:
To keep it simple, we could just modify the file list.txt after the above code, using this code (see all the explanatory rem remarks; the mentioned string manipulation is called sub-string substitution):
rem // Read file `list.txt` line by line:
(for /F "usebackq delims= eol=|" %%L in ("list.txt") do (
rem // Assign line string to variable:
set "LINE=%%L"
rem // Enable delayed expansion to be able to do string manipulation:
setlocal EnableDelayedExpansion
rem /* Replace every occurrence of ` - ` by a single character `|`, then use this one
rem as a delimiter to split the line string as `for /F` requires single-character
rem delimiters; just using `-` is not good as they might occur in the partial
rem strings that need to be kept, I suppose; the `|` must not occur in them: */
for /F "tokens=1 delims=| eol=|" %%K in ("!LINE: - =|!") do (
rem // Disable delayed expansion to not lose `!`-marks:
endlocal
rem // Return the split string, that is the part before the (first) ` - `:
echo %%K
)
)) > "list_NEW.txt"
The resulting data is contained in the file list_NEW.txt. To have it in the original file, append the following line to the code:
move /Y "list_NEW.txt" "list.txt" > nul
1... Usually the exit code and ErrorLevel are the same, but there are in fact some rare cases where they may differ.

Batch: Why does appending a textfile write "ECHO is off" instead?

So I wrote a batch that has some code to check how many times it has been run by reading a textfile and then writing back into that textfile the new, increased number.
#ECHO OFF
for /f "delims=" %%x in (TimesRun.txt) do set Build=%%x
set Build=%Build%+1
#echo Build value : %Build%
echo %Build%>>TimesRun.txt
Pause
That does append the textfile allright, but it adds "1+1" to it. Silly me! I forgot to use the /a switch to enable arithmetic operations! But when I change the code accordingly...
#ECHO OFF
for /f "delims=" %%x in (TimesRun.txt) do set Build=%%x
set /a Build=%Build%+1
#echo Build value : %Build%
echo %Build%>>TimesRun.txt
Pause
... something funny happens: Instead of appending my file, ECHO is off. gets written on the console. Now, I know that this usually happens when ECHO is used without text or with an empty variable. I have added the first #echo Build value : %Build% specifically to see whether the variable Build is empty or not; it is not, and the calculation was carried out correctly.
I already figured out that
>>TimesRun.txt (echo %Build%)
does bring the desired result. I still do not understand why
echo %Build%>>TimesRun.txt
does not, however. What am I missing?
You are unintentionally specifying a redirection handle.
Redirection allows you to specify a certain handle that defines what is to be redirected:
0 = STDIN (keyboard input)
1 = STDOUT (text output)
2 = STDERR (error text output)
3 ~ 9 = undefined
For the input redirection operator <, handle 0 is used by default; for the output redirection operators > and >>, the default handle is 1.
You can explicitly specify a handle by putting a single numeric figure in front of the redirection operator; for instance 2> defines to redirect the error text output.
In your echo command line you are doing exactly this unintentionally, when %Build% is a single numberic digit, like 1 for example:
echo 1>>TimesRun.txt
To avoid that, you have the following options:
To reverse the statement so that the redirection definition comes first:
>>TimesRun.txt echo %Build%
This is the most general and secure way of doing redirections.
To enclose the redirected command in parentheses:
(echo %Build%)>>TimesRun.txt
This also works safely.
To put a SPACE in front of the redirection operator:
echo %Build% >>TimesRun.txt
This works too, but the additional SPACE is included in the output of echo.
See also this great post: cmd.exe redirection operators order and position.
Batch file redirection can be customized to specify where you're outputting to.
command 1>file.txt redirects the output of STDOUT to file.txt
command 2>file.txt redirects the output of STDERR to file.txt
Your build value was 1, so you inadvertently told CMD to send the output of echo to TimesRun.txt - when you run echo by itself, it prints it's status (ON or OFF).
You also could have said echo %Build% >>TimesRun.txt and the space would prevent the value of Build from being treated as a redirection command.
The Microsoft article Using command redirection operators explains the 3 standard handles and how to redirect them to another handle, command, device, file or console application.
Redirection of output written to handle 1 - STDOUT - to a file should be done with just
using > ... create file if not already existing or overwrite existing file, or
using >> ... create file if not already existing or append to existing file.
The redirection operators are usually appended at end of a command line. But this is problematic in case of using command ECHO and the string output to STDOUT ends with 1 to 9.
One of several solutions is to specify in this case the redirection at beginning of the command line:
#for /F "delims=" %%x in (TimesRun.txt) do #set Build=%%x
#set /A Build+=1
#echo Build value : %Build%
>>TimesRun.txt echo %Build%
Executing this small batch file without #echo off at top from within a command prompt window shows what Windows command processor executes after preprocessing each line with text file TimesRun.txt containing currently the value 0 or does not exist at all.
echo 1 1>>TimesRun.txt
It can be seen that Windows command interpreter moved the redirection to end of line with inserting a space and 1 left to >>.
With above batch code the line with >> really executed after preprocessing is:
echo 2 1>>TimesRun.txt
Specifying the redirection at end with 1>>, i.e. use in the batch file
echo %Build%1>>TimesRun.txt
is also no good idea as this would result on first run in executing the line:
echo 11 1>>TimesRun.txt
So 11 is written into the file instead of 1. This wrong output could be avoided by inserting a space before >> or 1>>, i.e. use one of those two:
echo %Build% >>TimesRun.txt
echo %Build% 1>>TimesRun.txt
But then the space after %Build% is also written into the file as really executed is:
echo 1 1>>TimesRun.txt
The trailing space would be no problem here, but should be nevertheless avoided.
Note: On using arithmetic operations, i.e. set /A ... any string not being a number or operator is automatically interpreted as variable name and the current value of this variable is used on evaluating the arithmetic expression. Therefore after set /A with environment variable names consisting only of word characters and starting with an alphabetic character as usually used for environment variables no %...% or !...! must be used inside the arithmetic expression. This is explained in help of command SET output into console window on running set /? within a command prompt window.

Batch for writing month as numeral to txt file

I need a batch file that when run it saves the current month as a numeral to a text file;
ie; if it's April, it saves 04 to month.txt.
Also, it has to overwrite any other text that's already in the file.
ie; instead of the text finally looking like this:
04
04
04
04
-it just shows the most recent '04'. Also, no additional spaces or linebreaks should be added. Just the 04 (nothing more, nothing less).
Thanks to any repliers in advance. :)
There are the following problems for the task to be accomplished:
retrieving the current month (date) in a locale-independent manner;
not writing a (final) line-break to the output file;
not modifying the current ERRORLEVEL state;
I found the following solution (see below for an explanation):
#echo off
rem get locale-independent date/time string (YYYYMMDDHHMMSS...) by "WMIC" command;
for /F "tokens=2 delims==" %%I in ('wmic OS GET LocalDateTime /VALUE') do set DAT=%%I
rem the "%DAT:~4,2%" portion extracts the month numeral;
rem instead of just "echo"-ing the month numeral, use "set /P" user prompt text,
rem because this doesn't produce a final line-break unlike "echo" does;
rem then redirect the textual output of this to the text file;
rem since "set /P" modifies the ERRORLEVEL state, let's append the "cmd /C" portion;
(echo _| set /P DAT="%DAT:~4,2%" > month.txt) & cmd /C exit %ERRORLEVEL%
The set /P command is actually intended to prompt the user for a value to be assigned to a variable. In the script only the user prompt text is used. Unlike echo, the display string does not contain a final line-break. In order not to halt the script and awaiting any user input, an arbitrary echo string is piped in. (I use string "_" (echo _) rather than just a line-break (echo.) because the latter sets ERRORLEVEL to 1.)
Notice, that the variable assignment of set /P does actually not happen in our batch context, because the pipe creates a new context intermittently. In addition, the variable value is not of interest anyway, only the user prompt text. So you could also write set /P DUMMY= instead of set /P DAT=.
The double-quotes around the set /P user prompt text are needed to avoid a trailing space to be returned (even with the space before > removed). I do not why but I guess such might be caused by the way the redirection > is read by the command line interpreter.
If you are not interested in the ERRORLEVEL state, you may remove the cmd /C portion so only the part in between the parentheses is kept.
However, if you do not want the ERRORLEVEL state to be altered, keep the cmd /C portion, which restores the original ERRORLEVEL state, because:
the command interpreter reads the entire line prior to execution of any command;
%ERRORLEVEL% is expanded immediately, so its state prior to execution is stored;
as soon as set /P is executed, the actual ERRORLEVEL state is modified;
exit %ERRORLEVEL% sets the ERRORLEVEL state back to the previously stored original state;
cmd /C wrapped around the exit command avoids the current script to be terminated;
#echo off
echo %date:~3,2%>month.txt
Well, it is impossible (for me) not to make a line break, but try this:
echo %date:~3,2%>expected\destination\filename.txt
That's my answer. Whether accept it or not, hope it helps you!

How would I set each line of a text document to separate variables using batch?

How would I set a each line of a text document to separate variables using Batch? I know how to set a variable to the first line of a text document using:
Set /p Variable=<Test.txt
...but I don't know how to read other lines of the file. Lets say for example I had a text document with 3 lines, the first line had 'Apples' written on it, the second had 'Bananas' and the third had 'Pears', and lets say the document was called Fruit.txt. How would I set the variable 'Line_1' to the first line of the document, 'Line_2' to the second line and 'Line_3' to the last line?. Just to keep it simple, lets just say the batch file and Fruit.txt are both in the same folder. I don't want to do this in VBScript, so please only post Batch code. I would have thought that it would be something like:
#Echo off
Set /p Line_1=<Fruit.txt:1
Set /p Line_2=<Fruit.txt:2
Set /p Line_3=<Fruit.txt:3
Echo Fruit 1 is %Line_1%, Fruit 2 is %Line_2% and Fruit 3 is %Line_3%
Pause
Exit
...but quite clearly it isn't. Any help?
EDIT: This is for arbitrary-length files, then. jeb has an answer that solves your particular problem for a known number of lines. I will leave this here, though, as I hate deleting posts I put some time into for explanation :-)
Well, you obviously need some sort of counter. Let's start with 1:
set Counter=1
Then, you need to go line-wise through the file:
for /f %%x in (somefile) do ...
Then store the line in a numbered variable (that's what we have the counter for):
set "Line_!Counter!=%%x"
aaaaand increment the counter:
set /a Counter+=1
And that's it. Add a few more necessary things, you know, the boring stuff that's always needed in such cases (strange statements before and after, block delimiters, etc.), and you're done:
#echo off
setlocal enabledelayedexpansion
set Counter=1
for /f %%x in (somefile) do (
set "Line_!Counter!=%%x"
set /a Counter+=1
)
set /a NumLines=Counter - 1
Echo Fruit 1 is %Line_1%, Fruit 2 is %Line_2% and Fruit 3 is %Line_3%
rem or, for arbitrary file lengths:
for /l %%x in (1,1,%NumLines%) do echo Fruit %%x is !Line_%%x!
Some explanation:
set /p Var=<file will set the variable to the first line of a file, as you noted. That works because set /p will prompt for input and < file will redirect the file into standard input of a command. Thus set /p will interpret the file's contents as the entered input up until the user hits Return (i.e. the file contains a line break). That's why you get the first line. The system would throw the whole file at set /p but since the command only reads the first line and then is done they just get discarded.
The syntax you were proposing there is actually for accessing Alternate Data Streams of files on NTFS, which is somethhing totally different.
<short-detour> However, jeb has a way of reading multiple lines. This works because the block (delimited by parentheses) is a single command (see below) you can redirect a file's contents into. Except that command is comprised of multiple statements, each of which will read a single line and store it away. </short-detour>
Which brings us to for /f which iterates over the contents of a file (or the output of a command) line by line and executes a command or block of commands for each line. We can now read the whole file into as many variables as there are lines. We don't even need to know how many in advance.
You may have noticed the Line_!Counter! in there which uses Counter a little bit differently from how you're used to use environment variables, I guess. This is called delayed expansion and is necessary in some cases due to how cmd parses and executes batch files. Environment variables in a command are expanded to their values upon parsing that command. In this case the whole for /f including the block containing two statements is a single command for cmd. So if we used %Counter% it would be replaced by the value Counter had before the loop (1) and never change while the loop is running (as it is parsed once and run multiple times. Delayed expansion (signaled by using ! instead of % for variable access changes that and expands environment variables just prior to running a command.
This is almost always necessary if you change a variable within a loop and use it within the same loop again. Also this makes it necessary to first enable delayed expansion which is done with the setlocal command and an appropriate argument:
setlocal enabledelayedexpansion
set /a will perform arithmetic. We use it here to increment Counter by one for each line read.
To read multiple lines with set/p you need brackets around the set/p block.
#Echo off
(
Set /p Line_1=
Set /p Line_2=
Set /p Line_3=
) <Fruit.txt
Echo Fruit 1 is %Line_1%, Fruit 2 is %Line_2% and Fruit 3 is %Line_3%

How to avoid cmd.exe interpreting shell special characters like < > ^

I have a Windows CMD script that accepts a number of parameters and executes an EXE, passing first some hard-coded arguments and then all of the parameters from the user. The CMD script looks like this:
launcher.exe paramX paramY %*
The user would execute the CMD script from the Windows shell as follows:
launcher.cmd param1 param2 param3 [...]
The problem I have is that if the parameters to the CMD script contain shell special characters like < > and ^, the user is forced to escape these by preceding each with 3 caret ^ shell escape characters.
Two Examples
1) To pass the argument ten>one to the EXE, the user must launch the CMD as follows:
launcher.cmd ten^^^>one
The reason for this is that the shell special characters ^ and > are interpreted by the command shell at two levels, first on the command line and second inside the CMD script. So, the shell escaping with the caret ^ shell escape character must be applied twice. The problem is that this is non-obvious to the user and looks ugly.
For this example, a nicer solution is to surround the argument with double quotes. However, this breaks down for more complex examples that include a literal double quote in the argument.
2) To pass the argument "^ to the EXE, the user must launch the CMD as follows:
launcher.cmd "\"^^^^"
In my case I want to support arguments that contain any sequence of low ASCII characters, excluding control characters, i.e. code points 0x20 to 0x7E. I understand that there will be examples where the user will have to escape certain shell special characters with a caret. However, I don't want the user to have to use 3 carets every time in these cases just because they happen to be calling a CMD script instead of an EXE.
I can solve this problem by replacing the CMD script with an EXE that does the same. However, is there any way to alter the CMD script so that it passes its parameters through to the EXE without interpreting the shell special characters?
One way is to work with delayed expansion inside of the batch, because then the special characters lose there "special" meanings.
The only problem is to get the parameters into a variable.
Something like this could help
#echo off
setlocal DisableDelayedExpansion
rem ** At this point the delayedExpansion should be disabled
rem ** otherwise an exclamation mark in %1 can remove carets
set "param1=%~1"
setlocal EnableDelayedExpansion
rem ** Now you can use the param1, independent of the content, even with carets or quotes
rem ** but be careful with call's, because they start a second round of expansion
echo !param1!
set "tmp=!param1:~1,4!"
Now the parameters can be surround by quotation marks, so there the carets aren't neccessary anymore.
Example
launcher.bat "abc>def&geh%ijk|lmn^opq!"
The only remaining problematic special character seems to be the quotation mark.
[Edit/Improve]
I create another way to retrieve a parameter, I assume it can accept any string also your second example.
Even really hard strings like
launcher "^
launcher ten^>one
launcher "&"^&
#echo off
setlocal DisableDelayedExpansion
set "prompt=X"
for %%a in (1 ) do (
#echo on
for %%b in (4) do (
rem #%1#
)
) > XY.txt
#echo off
for /F "delims=" %%a in (xy.txt) DO (
set "param=%%a"
)
setlocal EnableDelayedExpansion
set param=!param:~7,-4!
echo param='!param!'
How it works?
The only way I have found to expand %1 without expanding the special characters like " or ^ is in a REM statement (For REM that's not completly true, but that is an other story)
Ok, the only problem is that a REM is a remark and has no effect :-)
But if you use echo on also rem lines are echoed before they are executed (execute for rem is a nice word).
The next problem is that it is displayed and you can not redirect this debug output with the normal > debug.txt.
This is also true if you use a for-loop.
Ok, you can redirect the echo on output with a call like
echo on
call :myFunc > debug.txt
But if you call a function you can't access the %1 of the batch file anymore.
But with a double for-loop, it is possible to activate the redirection for the debug output and it's still possible to access %1.
I change the prompt to "X", so I know it is always only one character long.
The only thing left is to explain why I append a # to %1.
That's because, some special characters are recognized in some situations even in a REM line, obviously ;-)
rem This is a remark^
rem This_is_a_multiline^
rem "This is also a multiline"^
So the # suppress a possible multiline situation.
Does this help:
EscapPipes.Cmd:
#echo off
:Start
If [%1]==[] goto :eof
#Echo %1
shift
goto :Start
When started thus:
EscapPipes.Cmd Andy Pandy "Pudding | > < and pie"
gives
Andy
Pandy
"Pudding | > < and pie"
As soon as you strip the quotes the pipe symbols will become live.

Resources