Powershell in a batch file - How do you escape metacharacters?

Viewed 1243

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):

  1. 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\""
  2. 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.

  3. 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.

  4. 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.

3 Answers

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%"
Related