5

I'm struggling with exclamation marks (!) on file and folder names. The batch I created reads the content of a .txt file and sort the listed files from a folder into a subfolder.
For example:
source.txt has inside: file_1.zip and file_2.zip
The folder called system has file_1.zip, file_2.zip, file_3.zip, etc. and the batch reads source.txt and copy both files into system/source.

I tried to avoid enabling enabling DelayedExpansion, but after adding the folder browser I couldn't figure out a better way than using it. I needed it enabled for the way I show :listSources and :listFolders My problem right now is not so much handling (!) on folder names (although it would be wonderful to allow it), but to allow the copy of the files having a (!) on its name.

I thought about escaping ^^! the character or toggling between setlocal and endlocal, but I'm still learning how to properly use it, thus no idea how to handle it. Maybe there is a better way? (I'm not a programmer!)

@echo off
setlocal EnableDelayedExpansion

rem Copy sorted files into destination folder? [Y/N]
set "copyMode=y"
if /i "%copyMode%"=="y" (set appendTitle=[Copy Mode]) else set appendTitle=[Log Mode]

rem Set prefix for output file (used only if copyMode=n)
set prefixMiss=missing_in_

rem Set defaults to launch directory
set rootSource=%~dp0
set rootFolder=%~dp0

:listSources
title Source file selection %appendTitle%
Show folder names in current directory
echo Available sources for sorting:
for %%a in (*.txt) do (
    set /a count+=1
    set mapArray[!count!]=%%a
    echo !count!: %%a
)

:checkSources
set type=source
if not exist !mapArray[1]! cls & echo No source file (.txt) found on current directory^^! & goto SUB_folderBrowser
if !count! gtr 1 set /a browser=!count!+1 & echo.!browser!: Open Folder Browser? & goto whichSource
if !count! equ 1 echo. & set /p "oneSource=Use !mapArray[1]! as source? [Y/N]: "
if /i "%oneSource%"=="y" (set "sourceFile=1" & echo. & goto listFolders) else goto SUB_folderBrowser

:whichSource
echo.
rem Select source file (.txt)
echo Which one is the source file you want to use? Choose !browser! to change directory.
set /p "sourceFile=#: "
if %sourceFile% equ !browser! goto SUB_folderBrowser
if exist !mapArray[%sourceFile%]! echo. & goto listFolders
echo Incorrect input^^! & goto whichSource

:listFolders
title System folder selection %appendTitle%
rem Show folder names in current directory
cd %~dp0
set count=0
echo Available folders for sorting:
for /d %%a in (*) do (
    set /a count+=1
    set mapArray2[!count!]=%%a
    echo !count!: %%a
)

:checkFolders
set type=root
if not exist !mapArray2[1]! cls & echo No folders found on current directory^^! & goto SUB_folderBrowser
if !count! gtr 1 set /a browser=!count!+1 & echo.!browser!: Open Folder Browser? & goto whichSystem
if !count! equ 1 echo. & set /p "oneFolder=Use !mapArray2[1]! as target? [Y/N]: "
if /i "%oneFolder%"=="y" (set "whichSystem=1" & goto whichFolder) else goto SUB_folderBrowser

:whichSystem
echo.
rem Select system folder
echo Which system do you want to sort? Choose !browser! to change directory.
set /p "whichSystem=#: "
if %whichSystem% equ !browser! goto SUB_folderBrowser
if exist !mapArray2[%whichSystem%]! echo. & goto createFolder
echo Incorrect input^^! & goto whichSystem

:createFolder
rem Create destination folder
if /i not "%copyMode%"=="y" goto sortFiles
set destFolder=!mapArray[%sourceFile%]:~0,-4!
if not exist ".\!mapArray2[%whichSystem%]!\%destFolder%" md ".\!mapArray2[%whichSystem%]!\%destFolder%"

:sortFiles
rem Look inside the source file and copy the files to the destination folder
title Sorting files in progress... %appendTitle%
for /f "delims=" %%a in ('type "%rootSource%\!mapArray[%sourceFile%]!"') do (
if exist ".\!mapArray2[%whichSystem%]!\%%a" (
    if /i "%copyMode%"=="y" copy ".\!mapArray2[%whichSystem%]!\%%a" ".\!mapArray2[%whichSystem%]!\%destFolder%\%%~nxa" >nul
    echo %%a
) else (
    echo.
    echo %%a missing & echo.
    if /i not "%copyMode%"=="y" echo %%a >> "%~dp0%prefixMiss%!mapArray2[%whichSystem%]!.txt"
    )
)

title Sorting files finished^^! %appendTitle%

popd & pause >nul & exit

:SUB_folderBrowser
if !count! lss 1 set /p "openBrowser=Open Folder Browser? [Y/N]: "
if !count! lss 1 (
    if not "%openBrowser%"=="y" exit
)
set count=0 & echo Opening Folder Browser... & echo.
if "%type%"=="root" echo Select the root system folder, not the system itself^^!

rem PowerShell-Subroutine to open a Folder Browser
set "psCommand="(new-object -COM 'Shell.Application')^
.BrowseForFolder(0,'Please choose a %type% folder.',0,0).self.path""
for /f "usebackq delims=" %%i in (`powershell %psCommand%`) do set "newRoot=%%i"

if "%type%"=="source" set rootSource=%newRoot%
if "%type%"=="root" set rootFolder=%newRoot%

rem Change working directory
if "%type%"=="source" cls & pushd %rootSource% & goto listSources
if "%type%"=="root" cls & pushd %rootFolder% & echo Selected source file: !mapArray[%sourceFile%]! & echo. & goto listFolders

Any help would be greatly appreciated!

paradadf
  • 123
  • 1
  • 7
  • 2
    You only need EnableDelayedExpansion in `For` loops. And in a `For` loop you can use `%%A` to access it so you may not need it at all. You can call a function `call :label %%A` and in that function it becomes `%1`. End a function with `Goto :EOF`. Also end the main procedure with it. –  Dec 29 '16 at 19:51
  • Thank you very much for your prompt answer. As I'm actually a complete noob that wrote that by doing research, I need time to do the proper thing with your answer (and anyone elses :D), try to implement it and see if I manage to achieve it somehow. Right now, I have no idea what you are suggesting, because my problem happens on for-loops. But for pointing me in the right direction, thank you very much! – paradadf Dec 29 '16 at 20:07
  • @Noodles His code doesn't include any call's, so inserting `Goto :Eof` won't help. @paraduff using only goto to control program flow quickly leads to [Spaghetti code](https://en.wikipedia.org/wiki/Spaghetti_code) no one will/can follow. –  Dec 29 '16 at 20:13
  • 1
    The reason why you have to turn on EnableDelayedExpansion is so files with `!` work normally in normal mode. `%Var%` is filled in with it's value when the line is read. `!Var!` is filled in when executed. Lines within brackets are read all at the same time as it's one line of execution. See my CMD cheat sheet here for general info http://stackoverflow.com/questions/41030190/command-to-run-a-bat-file/41049135#41049135 and read Help. `Call /?`, `For /?`, `Set /?`. –  Dec 29 '16 at 20:15
  • 2
    Related: [Batch: Reading in a line from a file that has a colon and exclamation](http://stackoverflow.com/a/41388147). – aschipfl Dec 29 '16 at 23:11
  • @Noodles I tried to work with your answer and tinker (with your help) something like this: **:listSources** `for %%a in (*.txt) do call :counter %%a` and the function: **:counter** `set /a count+=1` `set mapArray[%count%]=%1` `echo %count%: %1` `goto:eof` which works almost perfectly for file and folder names, but @dbenham's answer allows even characters like = ^, besides !. There are still problemswithin the last for-loop though, as I'm using sort of a variable within another variable :S – paradadf Dec 30 '16 at 13:56

1 Answers1

3

Delayed expansion only causes problems within FOR loops when the FOR variable contains !.

You can typically avoid delayed expansion within a FOR loop if you CALL out to a :subroutine from within the loop. The subroutine will be reparsed for each iteration, so you can use normal %var% variable expansion. But CALL slows things down considerably, so I like to avoid it.

There is a simple solution that avoids delayed expansion and avoids call - use DIR /B with FINDSTR /N to inject line numbers into the directory output, which FOR /F can easily parse.

For example, here is a solution for the ListSources loop:

for /f "delims=: tokens=1*" %%A in ('dir /b /a-d *.txt^|findstr /n "^"') do (
  set "mapArray[%%A]=%%B"
  set "count=%%A"
  echo %%A: %%B
)

You can easily apply the same technique to your second loop.

You still need delayed expansion in other non-loop sections of code to expand array values. So start with delayed expansion disabled, then enable it in :whichSource, disable it again before the 2nd loop, and then enable it one more time after the last loop. Note that you cannot use ENDLOCAL, else you will loose your previously defined array values. So you will create a small stack of SETLOCAL as you toggle between disabled and enabled delayed expansion.

dbenham
  • 127,446
  • 28
  • 251
  • 390
  • Hi @dbenham! I just tried it out and works beautifully, thanks! Even weird character on file and folder names like ^ = (besides !) work! This is what I have so far: [sorter.bat v2](http://pastebin.com/Axeshbkf). Your technique applied to the folder results in: `('dir /b /a:d ^| findstr /n "."')`. My problem now still has to do with the delayed expansion and it is only on **:sortFiles**. I need DelayedExpansion for the path, but `%%a` won't handle ! because of that. Could you give me some advice on that, please? – paradadf Dec 30 '16 at 15:34
  • I defined a variable before the :sortFiles for-loop and disabled expansion during execution. By doing that everything is working better than expected! You can find the latest working version of the batch [here](http://pastebin.com/WraUntEA). – paradadf Dec 30 '16 at 16:02
  • 3
    @paradadf - Excellent, but you have one problem. The maximum number of SETLOCAL within one CALL level is 32, so your loop in :sortfiles will fail if you have more than 32 iterations. This is solved by adding ENDLOCAL as the last command of the loop. This is OK because you are not setting any variables within the loop that must be preserved. – dbenham Dec 30 '16 at 16:20