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

北城以北 提交于 2019-12-09 06:27:40

问题


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.


回答1:


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.




回答2:


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



回答3:


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


来源:https://stackoverflow.com/questions/45666682/powershell-in-a-batch-file-how-do-you-escape-metacharacters

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!