Running Windows 7, when I copy a file to an external disk, during a routine file backup, I use Powershell v2 (run from a batch file) to re-create on the copy file all the timestamps of the original file.
The following code works successfully in most cases, but not always:-
SET file=%1
SET dest=E:\
COPY /V /Y %file% "%dest%"
SetLocal EnableDelayedExpansion
FOR /F "usebackq delims==" %%A IN ('%file%') DO (
SET fpath=%%~dpA
SET fname=%%~nxA
)
PowerShell.exe (Get-Item \"%dest%\%fname%\").CreationTime=$(Get-Item \"%fpath%%fname%\" ^| Select-Object -ExpandProperty CreationTime ^| Get-Date -f \"MM-dd-yyyy HH:mm:ss\")
The above code copies the file, then sets the creation date/time on the copy (destination) file to that of the source file, when I drag-and-drop the source file onto my batch file.
But there are some cases where the code fails. If the filename contains a 'poison' character, such as (for example) square brackets [...], it gives the error "Property 'CreationTime' cannot be found on this object". Parsing of the filename clearly fails at the 'poison' character.
The code does not give an error with symbols such as &.
I have tried a whole load of variations of escaping the Powershell command using both single and double quotes, but without success. Please can someone tell me how to escape those characters which Powershell objects to.
This is only a small section of a much longer batch routine, on which I rely in doing regular system backups. I don't have an option to switch to a .ps1 file instead, so I need a solution which works within a batch file, not in a .ps1 file.
Thanks for all suggestions.
ADDENDUM: I found a solution, by adopting one suggestion kindly supplied by mklement0. My problem with square brackets was overcome by substituting the following command for my original Powershell command -
PowerShell.exe (Get-Item -LiteralPath \"%dest%\%fname%\").CreationTime=$(Get-Item -LiteralPath \"%fpath%%fname%\" ^| Select-Object -ExpandProperty CreationTime ^| Get-Date -f \"MM-dd-yyyy HH:mm:ss\")
For future reference, please note that (on Windows 7):
The use of this revised command succeeds in preserving any extra whitespace characters. It is not necessary to include an extra pair of double-quote characters to achieve that.
- Editor's note: It's an edge case, but worth pointing out: without extra enclosing double quotes, any runs of more than one space are folded into one space; e.g.,
powershell.exe -command echo \"a b\"
yields a b
.
Enclosing the entire command in "..."
helps in principle -
powershell.exe -command "echo \"a b\""
- but since cmd.exe
then doesn't recognize the overall string as a single, double-quoted string, metacharacters can break the command; e.g.,
powershell.exe -command "echo \"a & b\""
It is not possible for the file path to include any " (double quote) character, so no code is required to escape that character. The double quote character is an illegal character in the FAT and NTFS filesystems, so can never be encountered in a file's path.
It is bad in principle to use ' (single quote) in the Powershell command, because that character is NOT illegal in the NTFS file system, so could be found in an actual path to a file. Use of double-quotes must be preferred, because the double-quote character, being illegal, can NEVER be encountered in an actual NTFS path.
With ROBOCOPY, the following wildcard solution succeeds even with most poison characters - all except ! (i.e. it can cope with = & ` ^). This command is pretty robust EVEN if there is more than one poison character (though not foolproof):
ROBOCOPY "%fpath% " "%dest%" "*%name%*%ext%*" /B /COPY:DAT /XJ /SL /R:0 /W:0 /V
a. The whitespace in "%fpath% " is ESSENTIAL, it is NOT an error.
b. The only poison character fatal in all circumstances is the EXCLAMATION MARK (!).
c. Poison characters only seem to be a problem in the FILENAME, not in the Path.
First things first:
In Windows 7, you can use robocopy.exe
to copy files, which preserves timestamps by default (and optionally gives you detailed control about what attributes get copied):
@echo off
:: Do NOT use setlocal ENABLEDELAYEDEXPANSION, because it would cause
:: misinterpretation of "!" chars. in filenames.
setlocal
:: Parse the file path given as %1 (the first argument) into its folder path and filename.
:: Be sure to pass the %1 argument *double-quoted* to prevent up-front interpretation
:: by cmd.exe; e.g.:
:: someBatchFile "c:\tmp\foo.txt" or someBatchFile "%file%"
:: Note that %~dp1 always returns a path with a trailing "\".
set "fpath=%~dp1"
set "fname=%~nx1"
:: Determine the destination folder
set "dest=E:\"
:: Use robocopy to copy the file to the destination dir. with timestamps preserved.
:: Syntax is: <source-dir> <dest-dir> <filename-or-wildcard> ...
:: IMPORTANT: To avoid problems with paths that end in "\", always follow
:: a variable reference inside "..." with a *space* (a trick discovered by
:: Ed999 himself).
robocopy "%fpath% " "%dest% " "%fname%"
Note:
While robocopy
is primarily used to copy entire directories, it does allow you to copy individual files, via wildcard expressions specified starting with the 3rd positional argument, as "%fname%"
above.
Given that robocopy
- unlike PowerShell - doesn't consider [
and ]
wildcard metacharacters, this approach should work (you'd only have a problem if your filenames contained embedded *
or ?
characters, which is unlikely).
The trailing-space trick (e.g., "%fpath% "
) is necessary, because robocopy
- as most command-line utilities do - treats a \"
at the end of an argument as an escaped "
, which breaks the command. Strictly speaking, the proper fix is to double the trailing \
(e.g., "E:\\"
), but you can get away with appending a space, because any trailing whitespace in paths is ignored. Thus, a simple way to make such calls robust is to always use "%var% "
(trailing space before the closing double quote) when passing folder paths.
robocopy
has no switch analogous to /V
that causes it to verify that the file was copied correctly, but - at least according to this blog post - running verify on
beforehand should have the same effect.
If you still need to copy the creation timestamp via PowerShell:
powershell -command "(Get-Item -LiteralPath '%dest%%fname%').CreationTime=(Get-Item -LiteralPath '%fpath%%fname%').CreationTime"
Caveat: If there's a chance that your filenames have embedded '
chars. (single quotes / apostrophes), you must escape them first, by doubling them (for instance, %fname:'=''%
returns the value of %fname%
with all '
instances doubled):
powershell -command "(Get-Item -LiteralPath '%dest:'=''%%fname:'=''%').CreationTime=(Get-Item -LiteralPath '%fpath:'=''%%fname:'=''%').CreationTime"
Note that the command as a whole is enclosed in "..."
, so as to prevent cmd.exe
metacharacters (such as &
) that may be contained in the variable values from breaking the command.[1]
Inside the command string, '...'
is used to ensure that the variable values are treated as literals by PowerShell (if you used "..."
and a filename contained $
, for instance, the result would be unexpected).
-LiteralPath
ensures that Get-Item
interprets the file path as a literal, whereas it is the
-Path
parameter that is implied when you pass the path positionally, and paths passed to
-Path
are interpreted as wildcard expressions, which can cause problems with PowerShell wildcard metacharacters such as [
and ]
.
There is no need to convert the .CreationTime
property value of the source file to a date+time string first; you can simply assign it directly to the target file's .CreationTime
property, which is of type [System.DateTime]
.
[1] Quoting headaches:
Enclosing only the variable references in \
-escaped double quotes, as in the question (\"%dest%\%fname%\"
), is not enough, because doing so subjects the value to whitespace normalization, meaning that runs of more than one space are normalized to a single space.
While additionally enclosing the command as a whole in "..."
helps in principle, cmd.exe
then doesn't recognize the overall string as a single, double-quoted string, in which case metacharacters such as &
in the variable values can break the command; e.g.,
powershell.exe -command "echo \"a b\""
works fine, faithfully preserving whitespace, but
powershell.exe -command "echo \"a & b\""
breaks, due to the &
.
- Using
\""
instead of \"
solves the metacharacter problem, but reintroduces whitespace normalization:
powershell.exe -command "echo \""a & b\"""
yields a & b
Therefore, using '...'
PowerShell strings inside the overall "..."
string is the simplest way to make the command robust: you only need to escape '
instances in the variable values.
In the final analysis, this was the best solution I could come up with in a batch file.
It works, 100% reliably, by making use of the REN and SET commands in the Windows command shell, which don't suffer the drawbacks of either ROBOCOPY or POWERSHELL.
@echo off
:: ** INPUT File **
SET file=%1
:: ** Destination Directory **
SET dest=C:\Users\%Username%\Desktop\test
:: ** Copy using Command Shell **
COPY /V /Y %file% "%dest%"
:: ** Location of PowerShell **
SET PowerShell=C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -NoProfile
:: ** Store PATH & NAME of source file (WITHOUT quotation marks) **
FOR /F "usebackq delims==" %%A IN ('%file%') DO (
SET fpath=%%~dpA
SET fname=%%~nxA
)
:: ** Rename the Files **
REN "%fpath%%fname%" temp1
REN "%dest%\%fname%" temp2
:: ** Set CREATED date of output file **
:: NB: Will fail if MONTHS (MM) is identical to MINUTES (mm)
%PowerShell% (Get-Item \""%dest%\temp2\"").CreationTime=$(Get-Item \""%fpath%temp1\"" ^| Select-Object -ExpandProperty CreationTime ^| Get-Date -f \"MM-dd-yyyy HH:mm:ss\")
:: ** Set MODIFIED date of output file **
:: NB: Will fail if MONTHS (MM) is identical to MINUTES (mm)
%PowerShell% (Get-Item \""%dest%\temp2\"").LastWriteTime=$(Get-Item \""%fpath%temp1\"" ^| Select-Object -ExpandProperty LastWriteTime ^| Get-Date -f \"MM-dd-yyyy HH:mm:ss\")
:: ** Wait **
:: No, I have no idea why it doesn't work without this...
echo. & echo Wait 15 Seconds ... & echo.
@CHOICE /T 15 /C yn /D y > NUL
:: ** Restore ORIGINAL filenames **
REN "%fpath%temp1" "%fname%"
REN "%dest%\temp2" "%fname%"
Rather than muddy the waters by tampering with the previous answers, here's my latest 'take' on this problem.
A month's additional fiddling about has led me to give up my previous solution (which worked great!) for one in which it is not necessary to rename the files (though I did find that a real neat idea: you can't run up against any poisonous metacharacters if you adopt my easy solution, by renaming the files to any harmless temporary name before running Powershell or Robocopy).
But below is a solution culled from the above ideas, which has, after a month of testing, not yet thrown up any failures. And it doesn't take any "unauthorised" shortcuts by renaming the file!
The following batch file now lives in my Windows 7 send-to folder, so is always accessible from the Windows Explorer right-click menu.
Windows 7 "send-to" folder:
C:\Users\%Username%\AppData\Roaming\Microsoft\Windows\SendTo
.
@echo off
:: *** Copy file including its CREATED date & MODIFIED date ***
:: File : Drag-and-Drop
SET file=%1
:: Destination Directory
SET dest=E:\
:: ** Safety Checks **
ATTRIB -R -A -S -H %file%
:: ** Store PATH & NAME of file (WITHOUT quotation marks) **
FOR /F "usebackq delims==" %%A IN ('%file%') DO (
SET fpath=%%~dpA
SET fname=%%~nxA
SET name=%%~nA
SET ext=%%~xA
)
:: *** POWERSHELL : File only ***
:: ** Copy File **
COPY /V /Y %file% "%dest%"
:: ** Location of PowerShell **
SET PowerShell=C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -NoProfile
:: ** Set CREATED date of copy **
%PowerShell% -command "(Get-Item -LiteralPath '%dest:'=''%\%fname:'=''%').CreationTime=(Get-Item -LiteralPath '%fpath:'=''%%fname:'=''%').CreationTime"
:: ** Set MODIFIED date of copy **
%PowerShell% -command "(Get-Item -LiteralPath '%dest:'=''%\%fname:'=''%').LastWriteTime=(Get-Item -LiteralPath '%fpath:'=''%%fname:'=''%').LastWriteTime"
:: ** Set ACCESSED date of copy **
%PowerShell% -command "(Get-Item -LiteralPath '%dest:'=''%\%fname:'=''%').LastAccessTime=(Get-Item -LiteralPath '%fpath:'=''%%fname:'=''%').LastAccessTime"
:: Open Destination Directory
C:\Windows\Explorer.exe "%dest%"