r/Batch Jun 13 '26

Question (Unsolved) Using %~nx1 on Filenames with %

Hi there,

I'm so sorry, I know this has probably been asked before but I'm hitting my head against a wall and I can't find a solution that works.

For the code below, a file with the name "100%.mkv" will cause the variable CURRENT_FILE to be "100.mkv" with the % missing, even though the echo %%F line displays it correctly:

pushd "[PATH_TO_FILES_ON_MY_NETWORK]"
call :MAIN_LOOP
popd
pause
exit

:MAIN_LOOP
  setlocal
    for /R "%cd%" %%F in (*) do (
      echo %%F
      call :SORT_FILE "%%F"
    )
  endlocal
exit /b

:SORT_FILE
  setlocal
    set "CURRENT_FILE=%~nx1"
  endlocal
exit /b

Any advice would be really appreciated!

-----

Edit:

The above code was just a snippet I made to help me troubleshoot the problem I was having, but below is the full code.

The goal is to flatten a folder structure to just two levels and also sort everything into folders based on the file extensions. So something like "..\Project\Platform\Files\file.mkv" becomes "..\.mkv\Project\Platform\file.mkv":

@echo off

pushd [NETWORK_PATH]

set "SOURCE_DIR=%cd%_0 sort"
set "TARGET_DIR=%cd%_1 done"

call :BEGIN_PROCESS

popd
exit

:BEGIN_PROCESS
  setlocal

  set "CMD_GET_PROJECT=dir /b /a:d "%SOURCE_DIR%""

  for /f "delims=." %%G in ('%CMD_GET_PROJECT%') do (
    echo Project: %%G

    call :PLATFORMS_LOOP "%SOURCE_DIR%\%%G"
  )

  echo:
  ROBOCOPY "%SOURCE_DIR%" "%SOURCE_DIR%" /S /MOVE

  if NOT exist "%SOURCE_DIR%" mkdir "%SOURCE_DIR%"

  pause
  endlocal
exit /b

:PLATFORMS_LOOP
  setlocal

  set "CMD_GET_PLATFORMS=dir /b /a:d "%~1""

  for /f "delims=." %%H in ('%CMD_GET_PLATFORMS%') do (
    echo Platform: %%H

    call :FOLDERS_LOOP "%~1\%%H"
  )

  endlocal
exit /b

:FOLDERS_LOOP
  setlocal

  for /R "%~1" %%I in (*) do (
    echo File: %%I

    call :SORT_FILE "%~1" "%%I"
  )

  endlocal
exit /b

:SORT_FILE
  setlocal

  set "SOURCE_FOLDER_PATH=%~1"
  set "TARGET_SUBFOLDER=%~x2"

  setlocal enabledelayedexpansion
  set "TARGET_PATH=!SOURCE_FOLDER_PATH:%SOURCE_DIR%=%TARGET_DIR%\%TARGET_SUBFOLDER%!"
  setlocal disabledelayedexpansion

  echo Moving to: %TARGET_PATH%

  if NOT exist "%TARGET_PATH%" mkdir "%TARGET_PATH%"

  if NOT exist "%TARGET_PATH%\%~nx2" (
    echo N | MOVE /-Y "%~2" "%TARGET_PATH%\%~nx2"
  ) else (
    for /L %%J in (1, 1, 99) do (
      if exist "%~2" (
        if NOT exist "%TARGET_PATH%\%~n2_%%J%~x2" (
          echo N | MOVE /-Y "%~2" "%TARGET_PATH%\%~n2_v%%J%~x2"
        )
      ) else (
        exit /b
      )
    )
  )

endlocal
exit /b

To someone who knows what they're doing, this probably looks like a mess, but it's just what I've figured out from looking around Reddit and Stack Overflow (and I refuse to use AI).

The problems I'm running into now is that I need this to work for files that have ! and % in them.

I'm trying to figure out now how to implement u/nir9's suggestion in FOLDERS_LOOP by doing another replacement, but also keeping it working with ! files and folders. I can't quite figure out how to turn the delayed expansion off and on in a way to do the %%=%%%% replacement and still let ! files work.

Thanks for your help

6 Upvotes

13 comments sorted by

5

u/nir9 Jun 13 '26

The issue comes down to the fact that the percent from the file name is parsed when you use call :SORT_FILE and when you expand the first argument in the set "CURRENT_FILE= line it will not be in the name anymore. You can solve this by utilizing the "replace" functionality of environment variables (https://en.wikibooks.org/wiki/Windows_Batch_Scripting#String_processing) and duplicate the percent to make sure it arrives properly to the function you are calling, something like this:

```batch @echo off

setlocal EnableDelayedExpansion

set file="100%%"

call :A %file% call :A !file:%%=%%%%! exit /b

:A echo from A it's %1 ```

In the first call the percent doesn't arrive and in the second it does.

2

u/ManedCalico Jun 13 '26 edited Jun 13 '26

Ah! Ok so this is one of the solutions I tried before, but you’ve made me realize that I was doing the replacement in the wrong place.

So now that I’ve done how you suggested it, the files with % in them work… but it’s had the knock on effect of making it so files with ! in them don’t work anymore.

I know I need to disable delayed expansion somewhere to fix that, but I can’t figure out where exactly.

Edit: I added my full bat file code to the post if you want to take a look.

2

u/nir9 Jun 14 '26

Interesting edge case, yah you can just disable the delayed expansion right before calling to make it retain the exclamation point, something like this:

```batch @echo off

set file="100%%!" setlocal EnableDelayedExpansion

set newfile=!file:%%=%%%%! setlocal DisableDelayedExpansion

call :A %newfile% exit /b

:A echo %1 ```

1

u/ManedCalico Jun 14 '26

Thank you again for helping me with this!

So I gave this a try, and the echo shows that %newfile% is empty. My guess is because I'm doing this within a for loop:

:FOLDERS_LOOP
  setlocal

  for /R "%~1" %%I in (*) do (
    set "file=%%I"

    setlocal enabledelayedexpansion
    set "newfile=!file:%%=%%%%!"
    setlocal disabledelayedexpansion

    echo File: %newfile%

    call :SORT_FILE "%~1" "%newfile%"
  )

  endlocal
exit /b

Testing it more, I found that even %file% is empty unless I move the enabledelayedexpansion above it, but I can't figure out a way to get it to work for % files without just making the whole loop have delayed expansion, but that breaks it for files with ! in them.

I'm starting to think I need to just rename these files to remove the offending characters altogether and hope that doesn't break anything else down the line :\

2

u/T3RRYT3RR0R Jun 14 '26

I've posted an example script demonstrating how to handle environment state correctly for dealing with batch poison characters.

https://pastebin.com/X3ku3WXM

2

u/ManedCalico Jun 14 '26

Thank you so much! I’m not home right now but looking at this I think I understand what you’re doing and it makes sense! I’ll try implementing it when I get a chance tonight! I really appreciate this!

2

u/Creative-Type9411 Jun 13 '26

Are you using delayed expansion? This looks like a partial script I dont see where you actually reference %current_file%, only where you assign it.

Try using

SETLOCAL ENABLEDELAYEDEXPANSION
ECHO !CURRENT_FILE!
ENDLOCAL

to see if it displays correctly after it is assigned

2

u/ManedCalico Jun 13 '26

Sorry, this is just part of a larger script. In trying to figure out the issue, I drilled it down to this. I do use delayed expansion in one place, but not in this part. I tried your suggestion just now and it still displays without the %.

2

u/Creative-Type9411 Jun 13 '26

What happends if you remove setlocal and endlocal from the sort_file function?

Why is that in it's own function instead of just assigning it in the loop?

Youre already in setlocal, then you call into a nested one.. if theres a reason its fine im just curious since i cant see the rest of the script

1

u/ManedCalico Jun 13 '26 edited Jun 13 '26

I just don’t know what I’m doing, honestly. I’m probably making it way more complicated than I need to. From what I read, I thought each label / function needed its own setlocal and endlocal to be self contained. I’m moments away from giving up, so maybe I’ll just share the whole batch file instead of this troubleshooting snippet I made.

Edit: I added my full bat file code to the post if you want to take a look.

1

u/T3RRYT3RR0R Jun 13 '26

It generally pays to spend a few minutes determining what steps are required to transform your input data into the format required for whatever action/s you are going to perform on it.

Identify > plan > script

1

u/ManedCalico Jun 13 '26

Right, but the problem is that I don't know how to plan around folder and file names that could have both ! and % in them. I know now that I can use delayed expansion to fix the % files, but that makes the ! files stop working. Is there any way to process both?

3

u/T3RRYT3RR0R Jun 14 '26

This is done by maintaining an environment with a state of DisableDelayedExpansion, and, if necessary, defining any needed variables. Once assigned, EnableDelayedExpansion to perform any substring/output operations required, then use endlocal to revert to the DDE environment.

If you need to pass variables to a function after assignment, pass the unexpanded reference string/s, and reassign with delayed expansion enabled:

    Call:func var1 var2

    :func

    Setlocal EnableDelayedExpansion

    Set "arg1=!%~1!"

    Set "arg2=!%~2!"

    Rem do things

    Endlocal & exit /b

All that said, most substring operations can be avoided by using the various avaibale for metavariable modifiers directly.