I created a batch file that searches for the users desktop folder in Windows.
for /F "skip=2 tokens=3* delims= " %%a in ('reg query "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" /v Desktop') do set DESKTOP=%%a
If my desktop folder isn't in C:\Users\User\Desktop, it will work and return the correct folder, for example in my case E:\User\Desktop. If the desktop folder is in C:\Users\User\Desktop, the script above will result into %USERPROFILE%\Desktop. Later in the script I try to create a new file on desktop. In the first option it will work, because E:\User\Desktop is a real directory. In the second one it won't because obviously %USERPROFILE%\Desktop doesn't count as a directory.
echo start javaw -jar "path/to/program.jar" >"%DESKTOP%\Start program.bat"
How to get it work on both situations?
Doing
if /I "%DESKTOP%" EQU "%USERPROFILE%\Desktop"
doesn't help because it is the same as
if /I "%DESKTOP%" EQU "C:\Users\User\Desktop"
You could use call to expand the variable:
call set desktop=%desktop%
You need an extra round of normal expansion. As wmz has already answered, you can get that extra round by using CALL.
You should be careful about special characters like & or ^ that are valid in folder and file names. I recommend using quotes in your assignment to protect special characters.
call set "DESKTOP=%DESKTOP%"
I am assuming the value of DESKTOP does not already include quotes. All bets are off if you have a mixture of definitions that sometimes include quotes, and other times don't.
Suppose DESKTOP=This & that\%VAR% and VAR=& the other thing. The end result with the quoted assignment using CALL will be correct: This & that\& the other thing.
I have seen & in folder names in the real world, so the quotes are a good thing. However, there is a problem if your value contains ^. A quoted ^ is doubled if passed through CALL.
Suppose DESKTOP=one^two%var% and VAR=^three, the end result of the quoted assignment using CALL will be one^^two^three.
I don't think I have ever run across ^ in a folder name, but it is possible, and there is a solution. You can use CMD /C to get another round of expansion, and use FOR /F to capture the result.
The following will give the correct result of one^two^three. I use "eol=:" because a valid path can never begin with :.
for /f "eol=: delims=" %%B in ('cmd /c echo "%DESKTOP%"') do set "DESKTOP=%%~B"
You do not need to use an intermediate assignment of DESKTOP. In all of the above, you can substitute your FOR /F %%a in place of %DESKTOP%.
Related
I want to create a batch file able to apply some processing on each JPG file in a folder hierarchy. The following script file works very well for that case (here I only echo the name of each file, but this should be replaced by some more complex statements in the real application):
:VERSION 1
#echo off
set "basefolder=C:\Base"
for /r %basefolder% %%f in (*.jpg) do echo %%f
Actually, I don't want to explore all the folder hierarchy under %basefolder%, but only a given list of subfolders. This modified script is able to deal with that case :
:VERSION 2
#echo off
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
pushd %basefolder%\%%~s"
for /r %%f in (*.jpg) do echo %%f
popd
)
Is there a solution to remove the pushd/popd pair of statements, to get something closer to the initial script. I thought that one of the following scripts would do the job:
:VERSION 3
#echo off
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
for /r %basefolder%\%%~s" %%f in (*.jpg) do echo %%f
)
or, using delayed expansion:
:VERSION 4
#echo off
setlocal enabledelayedexpansion
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
set "folder=%basefolder%\%%~s"
echo !folder!
for /r !folder! %%f in (*.jpg) do echo %%f
)
but none of them is working. When running the second one, the echo !folder! command in the external loop shows C:\Base\A, C:\Base\B and C:\Base\C as expected, but the inner loop doesn't echo any JPG file, so I guess that the recursive for /r command does not run correctly.
What am I doing wrong ?
Final edit after answers :
Thanks to #aschipfl who provided a link to the answer posted by #jeb on another question, quoted below:
The options of FOR, IF and REM are only parsed up to the special character phase. Or better the commands are detected in the special character phase and a different parser is activated then. Therefore it's neither possible to use delayed expansion nor FOR meta-variables in these options.
In other words, my versions 3 and 4 do not work because when defining the root folder of the FOR /R command, neither the %%~s nor the !folder! are correctly expanded by the expression parser. There is no way to change that, as this is a parser limitation. As I said in a comment below: the root folder option in the FOR /R command is basically only syntactic sugar to avoid the use of pushd/popd before and after the command. As this syntactic sugar is incomplete, we have to stick to the original syntax for some specific use cases, as the one presented here. The alternatives proposed by #Gerhard (using a subroutine CALL) or by #Mofi (parsing the result of a DIR command) are working, but they are neither more readable nor more efficient than the simple pushd/popd version I proposed initially.
My Approach for this would be really straight forward:
#echo off
set "basedir=C:\Base"
set "subfolders="A","B","C""
for %%i in (%subfolders%) do for /R "%basedir%" %%a in ("%%~i\*.jpg") do echo %%~fa
The double quotes inside of the subfolders variable is important here, it will ensure that folder names with whitespace are not seen as separators for the folder names. For instance:
set "subfolders="Folder A","Folder B","Folder C""
Edit
#echo off
set "basedir=C:\Base"
set "subfolders="A","B","C""
for %%i in (%subfolders%) do call :work "%%~i"
goto :eof
:work
for /R "%basedir%\%~1" %%a in (*.jpg) do echo %%~fa
It is in general not advisable to assign the value of a loop variable to an environment variable and next use the environment variable unmodified without or with concatenation with other strings being coded in batch file or defined already above the FOR loop within body of a FOR loop. That causes just problems as it requires the usage of delayed expansion which results in files and folders with one or more ! are not correct processed anymore inside body of the FOR loop caused by double parsing of the command line before execution, or command call is used on some command lines, or a subroutine is used called with call which makes the processing of the batch file much slower.
I recommend to use this batch file for the task:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "basefolder=C:\Base"
set "subfolders=A B C "Subfolder D" SubfolderE"
for %%I in (%subfolders%) do for /F "delims=" %%J in ('dir "%basefolder%\%%~I\*.jpg" /A-D /B /S 2^>nul') do echo %%J
endlocal
The inner FOR loop starts for each subfolder defined in subfolders in background one more command process with %ComSpec% /c and the DIR command line appended as additional arguments. So executed is with Windows installed to C:\Windows for example for the first subfolder:
C:\Windows\System32\cmd.exe /c dir "C:\Base\A\*.jpg" /A-D /B /S 2>nul
The command DIR searches
in specified directory C:\Base\A and all it subdirectories because of option /S
for files because of option /A-D (attribute not directory) including those with hidden attribute set
matching the pattern *.jpg in long or short file name
and outputs to handle STDOUT of background command process just the matching file names because of option /B (bare format)
with full path because of option /S.
The error message output by DIR on nothing found matching these criteria is redirecting from handle STDERR to device NUL to suppress it.
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.
The output to handle STDOUT of background command process is captured by FOR respectively the command process which is processing the batch file. FOR processes the captured output line by line after started cmd.exe terminated itself. This is very often very important. The list of files to process is already in memory of command process before processing the first file name. This is not the case on using for /R as this results in accessing file system, getting first file name of a non-hidden file matching the wildcard pattern, run all commands in body of FOR and accessing the file system once again to get next file name. The for /R approach is problematic if the commands in body of FOR change a file to process like deleting, moving, modifying, copying it in same folder, or renaming a found file because of the entries in file system changes while for /R is iterating over these entries. That can easily result in some files are skipped or some files are processed more than once and it could result also an endless running loop, especially on FAT file system like FAT32 or exFAT. It is never good to iterate over a list of files on which the list changes on each iteration.
Command FOR on usage of /F ignores empty lines which do not occur here. A non-empty line is split up into substrings using a normal space and a horizontal tab as string delimiters by default. This line splitting behavior is not wanted here as there could be full qualified file names containing anywhere inside full name one or more spaces. For that reason delims= is used to define an empty list of delimiters which disables the line splitting behavior.
FOR with option /F would also ignore lines on which first substring starts with ; which is the default end of line character. This is no problem here because of command DIR was used with option /S and so each file name is output with full path which makes it impossible that any file name starts with ;. So the default eol=; can be kept.
FOR with option /F assigns by default just first substring to specified loop variable as tokens=1 is the default. This default can be kept here as splitting the lines (full file names) into substrings is disabled already with delims= and so there is always the full file name assigned to the loop variable.
This example uses just echo %%I to output the file names with full path. But it is now safe to replace this single command by a command block which does more with the JPEG files because of the list of JPEG files for each specified subfolder tree in base folder is always already completely in memory of command process processing the batch file.
I have a string in a batch file, of the structure
[[status]]:{"01bcd123-1234-5678-0000-abcdefghijkl": "11"}
I need to get just the 01bcd123-1234-5678-0000-abcdefghijkl out of it, but trying to use " as a delimiter doesn't turn out well. \ and ^ don't seem to escape it properly.
set i=1
set "x!i!=%x:"=" & set /A i+=1 & set "x!i!=%"
Is what I have with x being the whole string, attempting to parse it into x1, x2 etc with " as the delimiter.
What is a proper way to split this string, using " as the delimiter?
Edit: Powershell tag is because I am running the script as part of a larger orchestration in Powershell and could export the functionality of the batch script into it if necessary.
Here are two approaches. The first one doesn't mess with the for syntax format, but it's risky - too much dependence on the string (the quotes are actually stripped by %%~). The second one is an ugly non-intuitive syntax, but actually delimits by quotes:
set "string=[[status]]:{"01bcd123-1234-5678-0000-abcdefghijkl": "11"}"
for /f "tokens=2 delims=:{" %%a in ("%string%") do #echo %%~a
for /f tokens^=2delims^=^" %%a in ("%string%") do #echo %%a
Well, the self-expanding code you have posted works fine, given that you have got delayed expansion enabled, by having put the statement setlocal EnableDelayedExpansion placed before. The string of interest is then stored in variable x2. Note that when the script terminates, x2 (like all the other x# variables as well) is no longer available since an implicit endlocal is executed then. To avoid that, place endlocal & set "x2=%x2%" in the last line:
#echo off
rem // Define string to parse:
set "x=[[status]]:{"01bcd123-1234-5678-0000-abcdefghijkl": "11"}"
rem // Enable delayed expansion:
setlocal EnableDelayedExpansion
rem // Initialise index counter:
set i=1
rem // Split string using self-expanding code:
set "x!i!=%x:"=" & set /A i+=1 & set "x!i!=%" & rem // (unbalanced `"`!)
rem // Display all `x#` variables:
set x
rem // Make `x2` survive the `endlocal` barrier:
endlocal & set "x2=%x2%"
rem // Return the retrieved value:
echo(%x2%
However, I would most probably use a for /F loop, but not with " as delimiter since the syntax appears quite odd then; rather I would use :, {, } and SPACE as delimiters. But I would remove the prefix [[status]] in advance:
#echo off
rem // Define string to parse:
set "x=[[status]]:{"01bcd123-1234-5678-0000-abcdefghijkl": "11"}"
rem /* At first, split off everything up to the first occurrence of `]]`;
rem if there is no such prefix, there is no harm, because nothing happens;
rem then extract the first token that is delimited by `:`, `{`, `}` or space;
rem that way there may even be spaces around the `:` or around `{` or `}`;
rem then return it with surrounding quotation marks removed (`~`-modifier): */
for /F "tokens=1 eol=: delims=:{} " %%I in ("%x:*]]=%") do echo(%%~I
N. B.:
The odd-looking syntax echo( is not a typo, it is actually the only safe way to echo an arbitrary string (even on, off or /?); take a look at this external thread for more details.
Since you tagged PowerShell, you can use the following regex, but I am not sure you want PowerShell based on the question.
[regex]::Match('[[status]]:{"01bcd123-1234-5678-0000-abcdefghijkl": "11"}','(?<=")[^"]+(?=")').Value
Split regex can also work:
('[[status]]:{"01bcd123-1234-5678-0000-abcdefghijkl": "11"}' -split '"')[1]
If you stick with a batch file, Stephan's helpful answer is definitely the simplest and fastest solution.
Needless to say, if you port your batch file to PowerShell, you'll have vastly more functionality at your disposal.
You can even harness that functionality from a batch file via PowerShell's CLI, by calling powershell.exe (Windows PowerShell) or pwsh.exe (POwerShell Core), but that comes with two caveats:
Doing so creates a PowerShell child process, whose startup time is not insignificant.
Getting nested quoting right can be a challenge, as shown below.
Here's a solution that calls PowerShell's CLI from a batch file, applying the -split technique from AdminOfThings' helfpul answer; again, this solution would be overkill in the case at hand, but the approach may be of interest if you need to perform tasks that simply cannot be done in the batch language or would be too cumbersome.
#echo off
setlocal
:: # The input text.
set txt=[[status]]:{"01bcd123-1234-5678-0000-abcdefghijkl": "11"}
:: # Call the PowerShell CLI to extract the token of interest and save the
:: # result in variable %id%.
:: # In PowerShell code, the equivalent would be:
:: # $id = ($txt -split '"')[1]
for /f %%i in ('powershell -noprofile -c "('%txt:"=\"%' -split '\""')[1]"') do set id=%%i
:: # Echo the result.
echo %id%
Note the need to \-escape the " chars. embedded in %txt%, via substitution %txt:"=\"%, and the need for an additional " char. after \" in '\""' so as to prevent the for command from breaking.
There are a handful of questions on SO that look similar, but I cannot figure out some behaviour and I am looking for help.
Below is a snippet from a batch file I am trying to write which will load in a set of directories and potentially replace letter substitutions with an expanded path, e.g. the properties file might look like:
location1=C:\Test
location2=[m]\Test
Where location1 points to C:\Test and location2 points to C:\Program Files(x86)\MODULE\Test, because [m] is a shorthand to C:\Program Files(x86)\MODULE.
The batch script, to this point, is simply trying to read in the list of file paths and expand/replace the [m].
SET build.dir=%~dp0%
SET progfiles=%PROGRAMFILES(X86)%
IF "%progfiles%"=="" SET progfiles=%ProgramFiles%
SET local.properties=%build.dir%local.properties
SETLOCAL ENABLEDELAYEDEXPANSION
FOR /F "tokens=1* delims==" %%i IN (%local.properties%) DO (
SET local.dir=%%j
SET local.dir=!local.dir:[m]=%progfiles%\MODULE!
echo !local.dir!
)
ENDLOCAL
Running this kicks out an error:
\MODULE was unexpected at this time.
If I replace the FOR with the following instead:
set test="[m]\Proj\Dir"
set test=!test:[m]=%progfiles%\MODULE!
echo %test%
I get the desired C:\Program Files(x86)\MODULE\Proj\Dir printed out...so I'm confused why it works fine outside of the FOR loop.
My understanding about delayed expansion is that it 'expands' at runtime...which you get to happen using !! instead of %% wrapped around the variable. Furthermore, as I'm creating the local.dir variable inside the FOR loop scope, I must use delayed expansion in order to access it with the updated value for the iteration.
I feel like the problem is using %progfiles%, like there's some special syntax I need to use in order to make it work but nothing is adding up for me. When I echo %progfiles%, it prints out as C:\Program Files(x86 -- note the missing trailing ).
Any ideas? Thanks
Tested suggestion:
D:\Projects\Test\Build>test
*** "D:\Projects\Test\Build\local.properties"
*** "","C:\Program Files (x86)"
[m]=C:\Program Files (x86)\MODULE
Adding quotes around the whole expression makes it work -- can't use other characters for some reason (like []) -- and since I want to append to the path later, we can safely remove the quotes afterwards:
SET local.dir="!local.dir:[m]=%progfiles%\MODULE!"
SET local.dir=!local.dir:"=!
Test this to see if you can nut out the issue:
The double quotes are to provide robust handling in a system with long file/path names.
The () are unquoted which are a problem in a batch script, when inside a loop.
#echo off
SET "build.dir=%~dp0%"
SET "progfiles=%PROGRAMFILES(X86)%"
IF "%progfiles%"=="" "SET progfiles=%ProgramFiles%"
SET "local.properties=%build.dir%local.properties"
echo *** "%local.properties%"
SETLOCAL ENABLEDELAYEDEXPANSION
FOR /F "usebackq tokens=1* delims==" %%i IN ("%local.properties%") DO (
SET "local.dir=%%j"
echo *** "!local.dir!","%progfiles%"
SET "local.dir=!local.dir:[m]=%progfiles%\MODULE!"
echo !local.dir!
)
ENDLOCAL
pause
It has to do with the () characters that end up in your progfiles string. If you take them out, the substitution seems to work fine.
My suggestion is to ditch command for this particular purpose and use one of the other standard tools that Windows comes with. While my personal preference would be Powershell (since it's so much more powerful and expressive), you may just need something quick that you can integrate into existing cmd.exe stuff.
In that case, try the following VBScript file, xlat.vbs:
set arg = wscript.arguments
wscript.echo Replace(arg(0),arg(1),arg(2))
Your batch file then becomes something like, noting the inner for /f which captures the output of the VBS script and assigns it to the variable:
#echo off
SET build.dir=%~dp0%
set progfiles=%PROGRAMFILES(X86)%
if "%progfiles%"=="" set progfiles=%ProgramFiles%
set local.properties=%build.dir%local.properties
setlocal enabledelayedexpansion
for /f "tokens=1* delims==" %%i in (%local.properties%) do (
set local.dir=%%j
for /f "delims=" %%x in ('cscript.exe //nologo xlat.vbs "!local.dir!" "[m]" "%progfiles%\MODULE"') do set local.dir=%%x
echo !local.dir!
)
endlocal
Running that, I get the output:
C:\Test
C:\Program Files (x86)\MODULE\Test
which I think is what you were after.
the code is:
setlocal EnableDelayedExpansion
FOR /f "usebackq tokens=*" %%X in (`dir /a-d /s /b "!search_path!" 2^>^&1`) DO #(
set file_path=%%X
rem do other stuff
)
Delayed expansion is on because the source path might have special characters like backticks percentages exclamation and ^ escape sing. All these characters are allowed in windows paths and I don't know if and where they will be present.
The problem arise what to do with double percent parameter %%X, how to pass it to another variable without expansion. If DE is on the exclamation sings will be treated as variables with and that would result with a range of weird errors. The same thing is if I disable DE - the same situation, but this time with percentages.
Any idea how to make these lines safe for every possible allowed path that can be found in windows system with no matter how weird characters ?
The problem boils to how to safe pass data from double percent for parameter into normal %variable% so the data can safe passed through delayed expansion from that moment.
I would try to adapt FOR /R to your needs, which will solve some of your escape efforts. You can check the format/match of the file listing in your loop vs. in the dir.
FOR /R will traverse your directory tree (which you're doing anyway) and return the files that match the pattern you give.
Quick example to list all files of type TXT in a directory and it's sub-directories goes like this:
UPDATED:
This prints the contents of two files in my directory that have exclamation points in them:
#echo off
for /r %%i in (ex*!*.txt) do (
type %%~i
)
Note the absense of delayed variable expansion. Add'l variable references are found at the bottom of the for /? listing.
I'm not sure whether it is possible, but what I need is a plain bat/cmd file that runs on windows 7 and does such things:
Step 1. findstr - it should find a specific string using regular expressions engine. Suppose we're looking for a number enclosed in tags <id>123</id> (suppose such a file is unique, so one value is returned). The command would print 123 to the screen, but I need to save it in a variable (don't know how).
Step 2. Another call to findstr on another directory. Now we want to find a file NAME (/m option) containing the value that we saved on Step 1 (in another group of files i.e. another directory). And again, save the result (name of the file) in a variable. Say, file_123.txt matches the criteria.
Step 3. Copy the file that we got as a result of the second findstr call (file_123.txt) to another location.
The whole question turns around the point about how to save the result of windows commands to variables to be able to provide these values to subsequent commands as parameters.
The general way of getting command output in variables is
for /f %%x in ('some command') do set Var=%%x
(with various variations, depending on context and what exactly is desired).
As for your steps, I elaborate after lunch. There are some intricacies.
Step 1:
FOR /F "USEBACKQ tokens=1-20 delims=<>" %%A in (`FINDSTR "123" "path of file to search in"`) DO (
SET var=%%B
)
ECHO %var%
Understand that delims will change depending on what 'separates' the parts of the output (whether its a space, a special character, etc.)
Step 2 & 3:
FOR /F "USEBACKQ tokens=*" %%A IN (`DIR "Path" /A ^| FIND /I "%var%"`) DO (
COPY /Y "%%A" "C:\New\Path\%%~nxA"
)