0

I'm trying to write a script which should read from a list of files and copy each item to a new location, following the same structure.

Originally, this was intended to copy files which were changed within a range of Git commits, but since I found an easier way (first a C# "script", then this), now I'm only doing it in the interest of learning.

This is an example of file_list.txt:

config.php
repository\file picker.php
user\edit.php

This is my .bat file so far:

@echo off

setlocal enabledelayedexpansion

set "source=input dir"
set "target=output dir"

for /f "tokens=* usebackq" %%A in ("file_list.txt") do (
    set "FILE=%%A"
    set "dest_file_full=%target%\!FILE:%source%=!"
    set "dest_file_filename=%%~nxA"
    ::set dest_file_dir=(!dest_file_full - !dest_file_filename)
    if not exist "!dest_file_dir!" (
        md "!dest_file_dir!"
    )
    set "source_file_full=%source%\!FILE:%source%=!"
    copy "!source_file_full!" "!dest_file_dir!"
)
pause

Notice the commented line set dest_file_dir=(!dest_file_full - !dest_file_filename). This is pseudo-code. I need to use variable substitution in here, but cmd.exe won't expand two local variables at one step.

I already tried a pipe...

echo set dest_file_dir=^!dest_file_full:!dest_file_filename!=^! | cmd

or using call:

call set dest_file_dir=!!dest_file_full:!dest_file_filename!=!!

But neither will work.

Am I doing something wrong in these, or is there an elegant way (hence without using a temp file) to accomplish this?

Thanks.

Community
  • 1
  • 1
Marc.2377
  • 7,807
  • 7
  • 51
  • 95

3 Answers3

1
FOR %%y IN ("!dest_file_full!") DO set "dest_file_dir=%%~dpy"

should get your destination drive+path+terminal \ into dest_file_dir.

needless to say, no doubt, the :: form of comment shouldn't be used in a block.

Magoo
  • 77,302
  • 8
  • 62
  • 84
1

This answer elaborates on expanding "nested variables" only:

In short, use this:

call set "dest_file_dir=%%dest_file_full:!dest_file_filename!=%%"

Escaping ^! or using call and !! does not work to expand nested variables with delayed expansion.

Pipes cannot help either as they initialise new cmd instances for both sides, which use their own separate environments.


Let us take a look at nested variables using immediate expansion first:

call set "dest_file_dir=%%dest_file_full:%dest_file_filename%=%%"

call introduces a second parsing phase, which is required to resolve the nesting. The call command receives a partially expanded command line, like this (supposing %dest_file_filename% expands to config.php, for example):

set "dest_file_dir=%dest_file_full:config.php=%"

This is nothing but the normal sub-string replacement syntax with immediate expansion, which is expanded during the said second parsing phase.

Note: This approach fails in case the value of dest_file_filename begins with %, ~, or * (but this is a forbidden character for file names anyway), or if it contains =! In addition, if present, " could cause trouble too (but such are also not allowed in file names)!


Now let us check out a similar approach but with delayed expansion enabled:

According to the post How does the Windows Command Interpreter (CMD.EXE) parse scripts?, call introduces a second parsing phase, but this does not include delayed expansion but immediate expansion only; so delayed expansion is only done during the first parsing phase.

Similar to the above solution using immediate expansion, we need to make sure that the search and replace strings of the sub-string replacement syntax are expanded during the first parsing phase, and that the overall expression is handled by the second one; so let us use this:

call set "dest_file_dir=%%dest_file_full:!dest_file_filename!=%%"

So the call command receives the following partially expanded command line:

set "dest_file_dir=%dest_file_full:config.php=%"

Again this constitutes the sub-string replacement syntax with immediate expansion.

Note: This approach fails in case the value of dest_file_filename begins with %, ~, or * (but this is a forbidden character for file names anyway), or if it contains =! In addition, if present, " could cause trouble too (but such are also not allowed in file names)!


An alternative method to introduce a second parsing phase needed for expanding nested variables is to use a for loop, like this:

for /F delims^=^ eol^= %%K in ("!dest_file_filename!") do set "dest_file_dir=!dest_file_full:%%K=!"

Here, !dest_file_filename! is expanded when the entire for /F loop is parsed; the second parsing phase applies to the body of the loop and receives the value of dest_file_filename (for instance, config.php) instead of %%K.

The advantage of this method is that immediate expansion is avoided completely, which could cause trouble in case " could occur in the value of dest_file_filename; however, this introduces problems with ! and ^ (they need to be escaped by preceding with ^).
The value of dest_file_filename must still not begin with ~ or * and must still not contain =. It must also not begin with !.

Community
  • 1
  • 1
aschipfl
  • 33,626
  • 12
  • 54
  • 99
0

String manipulation in (code blocks) gets sometimes confusing. I personally prefer to put these into a sub. This batch temporarily shuts off the prompt and Echo on to have a better output of your alterations:

@echo off
setlocal enabledelayedexpansion
Set MyPrompt=%Prompt%
Prompt $S
Echo on
set "source=input dir"
set "target=output dir"
for /f "tokens=* usebackq" %%A in ("file_list.txt") do call :sub "%%A"
pause
Prompt %MyPrompt%
Goto :Eof

:Sub
set "FILE=%~1"
set "dest_file_full=%target%\!FILE:%target%=!"
set "dest_file_filename=%~nx1"
set "dest_file_dir=!dest_file_full:%dest_file_filename%=!"
Rem if not exist "%dest_file_dir%" md "%!dest_file_dir%"
set "source_file_full=%source%\%FILE%"
@Echo copy "%source_file_full%" "%dest_file_dir%"

Especially the second set is unclear to me, more real vars should help.