Powershell в пакетном файле — как избежать метасимволов?

В Windows 7, когда я копирую файл на внешний диск, во время обычного резервного копирования файла я использую Powershell v2 (запуск из пакетного файла), чтобы воссоздать в файле копии все временные метки исходного файла.

Следующий код успешно работает в большинстве случаев, но не всегда:

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\")

Приведенный выше код копирует файл, а затем устанавливает дату/время создания копии (целевого) файла в исходный файл, когда я перетаскиваю исходный файл в свой пакетный файл.

Но бывают случаи, когда код не работает. Если имя файла содержит «ядовитый» символ, такой как (например) квадратные скобки [...], возникает ошибка «Свойство «CreationTime» не может быть найдено в этом объекте< /эм>". Синтаксический анализ имени файла явно не работает на символе «яд».

Код не выдает ошибку с такими символами, как &.

Я пробовал целую кучу вариантов экранирования команды Powershell, используя как одинарные, так и двойные кавычки, но безуспешно. Пожалуйста, может кто-нибудь сказать мне, как избежать тех символов, против которых возражает Powershell.

Это лишь небольшая часть гораздо более длинной пакетной процедуры, на которую я полагаюсь при регулярном резервном копировании системы. Вместо этого у меня нет возможности переключиться на файл .ps1, поэтому мне нужно решение, которое работает в пакетном файле, а не в файле .ps1.

Спасибо за все предложения.

ДОПОЛНЕНИЕ: я нашел решение, приняв одно предложение, любезно предоставленное mklement0. Моя проблема с квадратными скобками была решена путем замены моей исходной команды Powershell следующей командой:

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\")

Для справки в будущем обратите внимание на следующее (в Windows 7):

  1. Использование этой измененной команды позволяет сохранить любые лишние пробельные символы. Для этого нет необходимости включать дополнительную пару символов двойных кавычек.

    • 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. Невозможно, чтобы путь к файлу содержал какой-либо символ " (двойная кавычка), поэтому код для экранирования этого символа не требуется. Символ двойной кавычки является недопустимым символом в файловых системах FAT и NTFS, поэтому никогда не может встречаться в пути к файлу.

  3. В принципе, плохо использовать ' (одинарную кавычку) в команде Powershell, потому что этот символ НЕ является недопустимым в файловой системе NTFS, поэтому его можно найти в фактическом пути к файлу. Использование двойных кавычек должно быть предпочтительным, потому что символ двойных кавычек, будучи недопустимым, НИКОГДА не может быть обнаружен в фактическом пути NTFS.

  4. С ROBOCOPY следующее решение с подстановочными знаками успешно работает даже с большинством подозрительных символов — со всеми, кроме ! (т. е. оно может справиться с = & ` ^). Эта команда довольно надежна, ДАЖЕ если имеется больше символа отравления (хотя и не надежная):

    ROBOCOPY "%fpath% " "%dest%" "*%name%*%ext%*" /B /COPY:DAT /XJ /SL /R:0 /W:0 /V

    а. Пробел в "%fpath%" НЕОБХОДИМ, это НЕ ошибка.

    б. Единственный символ яда, смертельный при любых обстоятельствах, — это ВОСКРЕСИТЕЛЬНЫЙ ЗНАК (!).

    в. Ядовитые символы кажутся проблемой только в ИМЯ ФАЙЛА, а не в пути.


person Ed999    schedule 14.08.2017    source источник
comment
Хорошо поймать пробел в конце в "%path% ": проблема в том, что robocopy обрабатывает \" в конце аргумента как экранированный ", что прерывает команду. Строго говоря, правильное решение состоит в том, чтобы удвоить конечный \ (например, "E:\\"), но вы можете обойтись без добавления пробела, потому что любой конечный пробел в путях < я>проигнорировано. Таким образом, простой способ сделать такие вызовы надежными — всегда использовать "%var% " (конечный пробел перед закрывающей двойной кавычкой) при передаче путей к папкам. Я обновил свой ответ соответственно.   -  person mklement0    schedule 18.08.2017
comment
Только имена файлов подвержены интерпретации подстановочных метасимволов: действительно: robocopy поддерживает подстановочные знаки только в аргументах file (имя файла), но не в source и destination (папка) аргументы; пути к папкам всегда обрабатываются как литералы.   -  person mklement0    schedule 18.08.2017
comment
Что касается терминологии: пожалуйста, называйте позиционные символы (подстановочные знаки) метасимволами, что является устоявшимся и подходящим термином. ! проблематичен только потому, что вы используете enabledelayedexpansion - без него ! будет рассматриваться как литерал. Какие еще метасимволы вызывают проблемы?   -  person mklement0    schedule 18.08.2017
comment
Спасибо. Все ваши комментарии были бесценны. Большая проблема заключается в том, что = потому что ваши различные предложения позволили мне успешно справиться с ^&` и !   -  person Ed999    schedule 19.08.2017
comment
Не за что; ваш отзыв научил меня нескольким вещам. На самом деле я не вижу проблемы с = внутри командного файла, но нужно ли вам быть осторожным при вызове его со значением, содержащим =: всегда двойные кавычки такие пути, даже если они не содержат встроенных пробелов или других метасимволов; например, someBatchFile "c:\tmp\foo=bar.txt". Без двойных кавычек = действует как разделитель аргументов, точно так же, как и пробелы, и в итоге вы непреднамеренно передаете 2 аргумента, c:\tmp\foo и bar.txt. Это также применимо, когда вы передаете значение через переменную: используйте someBatchFile "%file%".   -  person mklement0    schedule 19.08.2017


Ответы (3)


Перво-наперво:

В Windows 7 вы можете использовать robocopy.exe для копирования файлов, что по умолчанию сохраняет метки времени (и при необходимости дает вам подробный контроль над тем, какие атрибуты копируются):

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

Примечание:

  • Хотя robocopy в основном используется для копирования целых каталогов, он действительно позволяет копировать отдельные файлы с помощью выражений с подстановочными знаками, указанных начиная с третьего позиционного аргумента. , как "%fname%" выше.
    Учитывая, что robocopy, в отличие от PowerShell, не учитывает подстановочные знаки [ и ] < em>метасимволы
    , этот подход должен работать (у вас возникнут проблемы только в том случае, если ваши имена файлов будут содержать встроенные символы * или ?, что маловероятно).

  • Трюк с завершающим пробелом (например, "%fpath% ") необходим, потому что robocopy, как и большинство утилит командной строки, обрабатывает \" в конце аргумента как экранированный ", что прерывает команду. Строго говоря, правильное решение состоит в том, чтобы удвоить конечные \ (например, "E:\\"), но вы можете обойтись без добавления пробела, потому что любой конечный пробел в путях < em>игнорируется. Таким образом, простой способ сделать такие вызовы надежными — всегда использовать "%var% " (конечный пробел перед закрывающей двойной кавычкой) при передаче путей к папкам.

  • robocopy не имеет переключателя, аналогичного /V, который заставляет его проверять правильность копирования файла, но, по крайней мере, в соответствии с этот пост в блоге — предварительное выполнение verify on должно иметь тот же эффект.


Если вам все еще нужно скопировать метку времени создания через PowerShell:

powershell -command "(Get-Item -LiteralPath '%dest%%fname%').CreationTime=(Get-Item -LiteralPath '%fpath%%fname%').CreationTime"

Предупреждение: если есть вероятность, что ваши имена файлов содержат ' символов. (одинарные кавычки/апострофы), вы должны сначала экранировать их, удвоив их (например, %fname:'=''% возвращает значение %fname% со всеми удвоенными экземплярами '):

powershell -command "(Get-Item -LiteralPath '%dest:'=''%%fname:'=''%').CreationTime=(Get-Item -LiteralPath '%fpath:'=''%%fname:'=''%').CreationTime"
  • Обратите внимание, что вся команда заключена в "...", чтобы предотвратить нарушение команды метасимволами cmd.exe (такими как &), которые могут содержаться в значениях переменных.[1]

  • Внутри командной строки используется '...', чтобы убедиться, что значения переменных обрабатываются PowerShell как литералы (например, если вы использовали "..." и имя файла содержало $, результат было бы неожиданно).

  • -LiteralPath гарантирует, что Get-Item интерпретирует путь к файлу как литерал, в то время как
    параметр -Path подразумевается, когда вы передаете путь позиционно, а пути, переданные в< br> -Path интерпретируются как выражения с подстановочными знаками, что может вызвать проблемы с метасимволами подстановки PowerShell, такими как [ и ].

  • Нет необходимости сначала преобразовывать значение свойства .CreationTime исходного файла в строку даты и времени; вы можете просто присвоить его непосредственно свойству .CreationTime целевого файла, имеющему тип [System.DateTime].


[1] Цитируя головные боли:

  • Заключить только ссылки на переменные в двойные кавычки с экранированием \, как в вопросе (\"%dest%\%fname%\"), недостаточно, поскольку это подвергает значение нормализации пробелов , что означает, что прогоны из более одного пробела нормализуются к одному пробелу.

  • Хотя дополнительно включение команды в целом в "..." в принципе помогает, cmd.exe тогда не распознает общую строку как одну строку в двойных кавычках, и в этом случае метасимволы такие как & в значениях переменных могут сломать команду; например,
    powershell.exe -command "echo \"a b\"" работает нормально, точно сохраняя пробелы, но
    powershell.exe -command "echo \"a & b\"" ломается из-за &.

    • Using \"" instead of \" solves the metacharacter problem, but reintroduces whitespace normalization:
      powershell.exe -command "echo \""a & b\""" yields a & b
  • Таким образом, использование строк '...' PowerShell внутри общей строки "..." — самый простой способ сделать команду надежной: вам нужно всего лишь экранировать ' экземпляров в значениях переменных.

person mklement0    schedule 14.08.2017

В конечном счете, это было лучшее решение, которое я мог придумать в пакетном файле.

Он работает со 100% надежностью благодаря использованию команд REN и SET в командной оболочке Windows, которые лишены недостатков ROBOCOPY или 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%"
person Ed999    schedule 31.08.2017
comment
У вас случайно многопоточность, как у robocopy? - person HPWD; 21.11.2020
comment
@HPWD — единственный способ активировать многопоточность в ROBOCOPY — это добавить необязательный переключатель /MT. Вы заметите, что мой сценарий не включает эту опцию. Я использую Windows 7 и обнаружил, что на практике эта опция вызывает больше проблем, чем решает. Если вы используете Windows 8 или 10, возможно, ваш опыт будет другим; но я рекомендую не использовать этот вариант. В моих тестах это не улучшило производительность этого скрипта. - person Ed999; 08.12.2020
comment
Я пытаюсь сказать, что обнаружил серьезные проблемы с использованием ROBOCOPY, когда тестировал его с параметром многопоточности /MT, поэтому я отказался от этого подхода на раннем этапе. Затем я вообще отказался от использования ROBOCOPY в этом контексте, потому что у него слишком много проблем. Таким образом, я порекомендовал гораздо лучшее решение POWERSHELL. Я не пытался использовать многопоточность в POWERSHELL. И поскольку я давно отказался от использования многопоточности в ROBOCOPY, я не могу сказать, дает ли она какие-либо преимущества. Я ничего не нашел. Ни один из сценариев, которые я разместил здесь, не использует его. - person Ed999; 13.12.2020

Вместо того, чтобы мутить воду, вмешиваясь в предыдущие ответы, вот мой последний «взгляд» на эту проблему.

Месяц дополнительных возни привел меня к тому, что я отказался от своего предыдущего решения (которое отлично сработало!) в пользу решения, в котором не нужно переименовывать файлы (хотя я обнаружил, что настоящий отличная идея: вы не сможете столкнуться с опасными метасимволами, если примете мое простое решение, переименовав файлы в любое безопасное временное имя перед запуском Powershell или Robocopy).

Но ниже приведено решение, основанное на приведенных выше идеях, которое после месяца тестирования еще не дало никаких сбоев. И он не использует никаких "неавторизованных" ярлыков, переименовывая файл!

Следующий командный файл теперь находится в моей папке отправки Windows 7, поэтому он всегда доступен из контекстного меню проводника Windows.

Папка для отправки в Windows 7:

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%"
person Ed999    schedule 24.09.2017