Как преобразовать короткий путь Windows в длинные имена в пакете script

Я пишу пакет Windows script, и у меня есть аргумент или переменная, содержащая путь, который использует короткие имена 8.3. Путь может представлять файл или папку.

Как преобразовать путь 8.3 в длинный путь? Как минимум, я хотел бы просто распечатать полный длинный путь. Но в идеале я хотел бы безопасно получить длинный путь в новую переменную.

Например, учитывая путь C:\PROGRA~1\AVASTS~1\avast\BROWSE~1.INI, я хотел бы вернуть C:\Program Files\AVAST Software\avast\BrowserCleanup.ini.

Как энтузиаст партии, меня больше всего интересует чистое пакетное решение, которое использует только собственные команды Windows. Но использование hybird других собственных скриптовых инструментов, таких как PowerShell и JScript, также приемлемо.

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

Ответ 1

Сначала я покажу, как преобразовать аргумент пакетного файла %1 и вывести результат на экран.

PowerShell

Самое простое решение - использовать PowerShell. Я нашел следующий код в блоге MSDN Сергея Бабкина

$long_path = (Get-Item -LiteralPath $path).FullName

Поместить этот код в пакетный скрипт и напечатать результат тривиально:

@echo off
powershell "(Get-Item -LiteralPath '%~1').FullName"

Однако я стараюсь избегать использования PowerShell в пакетном режиме по двум причинам.

  • PowerShell не является родным для XP
  • Время запуска PowerShell значительно, поэтому пакетный гибрид становится относительно медленным


CSCRIPT (JScript или VBS)

Я нашел этот фрагмент VBS на форуме Computer Hope, который использует фиктивный ярлык для преобразования из короткой формы в длинную.

set oArgs = Wscript.Arguments
wscript.echo LongName(oArgs(0))
Function LongName(strFName)
Const ScFSO = "Scripting.FileSystemObject"
Const WScSh = "WScript.Shell"
   With WScript.CreateObject(WScSh).CreateShortcut("dummy.lnk")
     .TargetPath = CreateObject(ScFSO).GetFile(strFName)
     LongName = .TargetPath
   End With
End Function

Я нашел похожий код в архиве группы новостей Microsoft и на старом форуме VBScript.

Код поддерживает только пути к файлам, и немного проще встроить JScript в пакет. После преобразования в JScript и добавления обработчика исключений для получения папки в случае сбоя файла я получаю следующий гибридный код:

@if (@X)==(@Y) @end /* Harmless hybrid line that begins a JScript comment

::----------- Batch Code-----------------
@echo off
cscript //E:JScript //nologo "%~f0" %1
exit /b

------------ JScript Code---------------*/
var shortcut = WScript.CreateObject("WScript.Shell").CreateShortcut("dummy.lnk");
var fso = new ActiveXObject("Scripting.FileSystemObject");
var folder='';
try {
  shortcut.TargetPath = fso.GetFile(WScript.Arguments(0));
}
catch(e) {
  try {
    shortcut.TargetPath = fso.GetFolder(WScript.Arguments(0));
    folder='\\'
  }
  catch(e) {
    WScript.StdErr.WriteLine(e.message);
    WScript.Quit(1);
  }
}
WScript.StdOut.WriteLine(shortcut.TargetPath+folder);


Чистая партия

Удивительно, но мой веб-поиск не смог найти чисто пакетное решение. Так что я был один.

Если вы знаете, что путь представляет файл, тогда преобразовать имя файла 8.3 в длинное имя просто, используя dir/b "yourFilePath". Однако это не разрешает имена родительских папок.

Ситуация еще хуже, если путь представляет папку. Невозможно перечислить конкретную папку, используя только команду DIR - она всегда перечисляет содержимое папки, а не само имя папки.

Я попробовал несколько стратегий для обработки путей к папкам, но ни одна из них не сработала:

  • CD или PUSHD к пути и затем посмотрите на подсказку - она сохраняет короткие имена папок
  • XCOPY с параметрами /L и /F - он также сохраняет короткие имена папок
  • Аргумент или модификатор переменной FOR %~f1 или %%~fA - сохраняет короткие имена
  • FORFILES - не поддерживает короткие имена.

Единственное решение, которое мне удалось найти, - это использовать DIR для итеративного преобразования каждой папки в пути, по одной за раз. Это требует, чтобы я использовал DIR/X/B/AD чтобы вывести список всех папок в родительской папке, включая их имена 8.3, а затем использую FINDSTR, чтобы найти правильное короткое имя папки. Я полагаюсь на то, что короткое имя файла всегда появляется в одном и том же месте после текста <DIR>. Как только я найду правильную строку, я могу использовать переменную подстроку или операции поиска/замены, или FOR/F для анализа длинного имени папки. Я решил использовать FOR/F.

Еще один камень преткновения, который у меня был, заключался в том, чтобы определить, представляет ли исходный путь файл или папку. Часто используемый подход добавления обратной косой черты и использования IF EXIST "yourPath\" echo FOLDER неправильно сообщает о файле как папке, если путь включает символическую ссылку или соединение, что является обычным в сетевых средах компании.

Я решил использовать IF EXIST "yourPath\*", который можно найти по адресу fooobar.com/questions/19838/....

Но также можно использовать модификатор атрибута FOR для переменной %%~aF для поиска атрибута d (directory), который можно найти по адресу fooobar.com/questions/19838/... и fooobar.com/questions/81859/....

Итак, вот полностью работающее чистое пакетное решение

@echo off
setlocal disableDelayedExpansion

:: Validate path
set "test=%~1"
if "%test:**=%" neq "%test%" goto :err
if "%test:?=%"  neq "%test%" goto :err
if not exist "%test%"  goto :err

:: Initialize
set "returnPath="
set "sourcePath=%~f1"

:: Resolve file name, if present
if not exist "%~1\*" (
  for /f "eol=: delims=" %%F in ('dir /b "%~1"') do set "returnPath=%%~nxF"
  set "sourcePath=%~f1\.."
)

:resolvePath :: one folder at a time
for %%F in ("%sourcePath%") do (
  if "%%~nxF" equ "" (
    for %%P in ("%%~fF%returnPath%") do echo %%~P
    exit /b 0
  )
  for %%P in ("%sourcePath%\..") do (
    for /f "delims=> tokens=2" %%A in (
      'dir /ad /x "%%~fP"^|findstr /c:">          %%~nxF "'
    ) do for /f "tokens=1*" %%B in ("%%A") do set "returnPath=%%C\%returnPath%"
  ) || set "returnPath=%%~nxF\%returnPath%"
  set "sourcePath=%%~dpF."
)
goto :resolvePath

:err
>&2 echo Path not found
exit /b 1

GOTO, используемый для итерации отдельных папок, замедлит работу, если будет много папок. Если бы я действительно хотел оптимизировать скорость, я мог бы использовать FOR/F, чтобы вызвать другой пакетный процесс, и разрешить каждую папку в бесконечном цикле FOR/L %%N IN() DO..., и использовать EXIT чтобы выйти из петля, как только я достигну корня. Но я не стал беспокоиться.


Разработка надежных утилит, которые могут возвращать результат в переменной

Существует ряд крайних случаев, которые могут усложнить разработку надежного сценария, учитывая, что ^, % и ! все допустимые символы в именах файлов/папок.

  • CALL удваивает кавычки ^ символов. Нет хорошего решения этой проблемы, кроме как передать значение по ссылке, используя переменную вместо строкового литерала. Это не проблема, если путь ввода использует только короткие имена. Но это может быть проблемой, если путь использует смесь коротких и длинных имен.

  • Передача литералов % в пакетных аргументах может быть сложной. Может возникнуть путаница относительно того, кого много раз (если вообще) следует удвоить. Опять же, может быть проще передать значение по ссылке в переменной.

  • CALLer может вызвать утилиту из цикла FOR. Если переменная или аргумент содержит %, то расширение %var% или %1 внутри цикла в утилите может привести к непреднамеренному расширению переменной FOR, поскольку переменные FOR являются глобальными по объему. Утилита не должна расширять аргументы в цикле FOR, а переменные могут быть безопасно развернуты только в цикле FOR, если используется отложенное расширение.

  • Расширение переменных FOR, содержащих ! будет повреждено, если включено отложенное расширение.

  • В среде CALLing может быть включено или отключено отложенное расширение. Передача значений, содержащих ! и ^ через ЭНДЛОКАЛЬНЫЙ барьер для среды с отложенным расширением требуется указанное ! бежать как ^! , Кроме того, кавычка ^ должна быть экранирована как ^^, но только если строка содержит ! , Конечно, эти символы не должны быть экранированы, если в среде CALLing отключено отложенное расширение.

Я разработал надежные вспомогательные формы как JScript, так и чисто пакетных решений, которые учитывают все крайние случаи, описанные выше.

Утилиты ожидают путь как строковый литерал по умолчанию, но принимают имя переменной, которая содержит путь, если используется параметр /V

По умолчанию утилиты просто выводят результат на стандартный вывод. Но результат может быть возвращен в переменной, если вы передаете имя возвращаемой переменной в качестве дополнительного аргумента. Верное значение гарантированно будет возвращено, независимо от того, включено или отключено отложенное расширение в вашей среде CALLing.

Полная документация встроена в утилиты и доступна с помощью /? вариант.

Есть несколько неясных ограничений:

  • Имя возвращаемой переменной не должно содержать ! или % символов
  • Аналогично, имя входной переменной опции /V не должно содержать ! или % символов.
  • Входной путь не должен содержать внутренних двойных кавычек. Можно указывать путь в одном наборе двойных кавычек, но внутри не должно быть никаких дополнительных кавычек.

Я не проверял, работают ли утилиты с юникодом в именах путей, или они работают с путями UNC.


jLongPath.bat - гибридный JScript/пакетный

@if (@X)==(@Y) @end /* Harmless hybrid line that begins a JScript comment
:::
:::jLongPath  [/V]  SrcPath  [RtnVar]
:::jLongPath  /?
:::
:::  Determine the absolute long-name path of source path SrcPath
:::  and return the result in variable RtnVar.
:::
:::  If RtnVar is not specified, then print the result to stderr.
:::
:::  If option /V is specified, then SrcPath is a variable that
:::  contains the source path.
:::
:::  If the first argument is /?, then print this help to stdout.
:::
:::  The returned ERROLEVEL is 0 upon success, 1 if failure.
:::
:::  jLongPath.bat version 1.0 was written by Dave Benham
:::

::----------- Batch Code-----------------
@echo off
setlocal disableDelayedExpansion
if /i "%~1" equ "/?" (
  for /f "tokens=* delims=:" %%A in ('findstr "^:::" "%~f0"') do @echo(%%A
  exit /b 0
)
if /i "%~1" equ "/V" shift /1
(
  for /f "delims=* tokens=1,2" %%A in (
    'cscript //E:JScript //nologo "%~f0" %*'
  ) do if "%~2" equ "" (echo %%A) else (
    endlocal
    if "!!" equ "" (set "%~2=%%B" !) else set "%~2=%%A"
  )
) || exit /b 1
exit /b 0

------------ JScript Code---------------*/
try {
  var shortcut = WScript.CreateObject("WScript.Shell").CreateShortcut("dummy.lnk"),
      fso = new ActiveXObject("Scripting.FileSystemObject"),
      path=WScript.Arguments(0),
      folder='';
  if (path.toUpperCase()=='/V') {
    var env=WScript.CreateObject("WScript.Shell").Environment("Process");
    path=env(WScript.Arguments(1));
  }
  try {
    shortcut.TargetPath = fso.GetFile(path);
  }
  catch(e) {
    shortcut.TargetPath = fso.GetFolder(path);
    folder='\\'
  }
  var rtn = shortcut.TargetPath+folder+'*';
  WScript.StdOut.WriteLine( rtn + rtn.replace(/\^/g,'^^').replace(/!/g,'^!') );
}
catch(e) {
  WScript.StdErr.WriteLine(
    (e.number==-2146828283) ? 'Path not found' :
    (e.number==-2146828279) ? 'Missing path argument - Use jLongPath /? for help.' :
    e.message
  );
}


longPath.bat - Чистая партия

:::
:::longPath  [/V]  SrcPath  [RtnVar]
:::longPath  /?
:::
:::  Determine the absolute long-name path of source path SrcPath
:::  and return the result in variable RtnVar.
:::
:::  If RtnVar is not specified, then print the result to stderr.
:::
:::  If option /V is specified, then SrcPath is a variable that
:::  contains the source path.
:::
:::  If the first argument is /?, then prints this help to stdout.
:::
:::  The returned ERROLEVEL is 0 upon success, 1 if failure.
:::
:::  longPath.bat version 1.0 was written by Dave Benham
:::
@echo off
setlocal disableDelayedExpansion

:: Load arguments
if "%~1" equ "" goto :noPath
if "%~1" equ "/?" (
  for /f "tokens=* delims=:" %%A in ('findstr "^:::" "%~f0"') do @echo(%%A
  exit /b 0
)
if /i "%~1" equ "/V" (
  setlocal enableDelayedExpansion
  if "%~2" equ "" goto :noPath
  if not defined %~2!! goto :notFound
  for /f "eol=: delims=" %%F in ("!%~2!") do (
    endlocal
    set "sourcePath=%%~fF"
    set "test=%%F"
  )
  shift /1
) else (
  set "sourcePath=%~f1"
  set "test=%~1"
)

:: Validate path
if "%test:**=%" neq "%test%" goto :notFound
if "%test:?=%"  neq "%test%" goto :notFound
if not exist "%test%" goto :notFound

:: Resolve file name, if present
set "returnPath="
if not exist "%sourcePath%\*" (
  for /f "eol=: delims=" %%F in ('dir /b "%sourcePath%"') do set "returnPath=%%~nxF"
  set "sourcePath=%sourcePath%\.."
)

:resolvePath :: one folder at a time
for /f "delims=* tokens=1,2" %%R in (^""%returnPath%"*"%sourcePath%"^") do (
  if "%%~nxS" equ "" for %%P in ("%%~fS%%~R") do (
    if "%~2" equ "" (
      echo %%~P
      exit /b 0
    )
    set "returnPath=%%~P"
    goto :return
  )
  for %%P in ("%%~S\..") do (
    for /f "delims=> tokens=2" %%A in (
      'dir /ad /x "%%~fP"^|findstr /c:">          %%~nxS "'
    ) do for /f "tokens=1*" %%B in ("%%A") do set "returnPath=%%C\%%~R"
  ) || set "returnPath=%%~nxS\%%~R"
  set "sourcePath=%%~dpS."
)
goto :resolvePath

:return
set "delayedPath=%returnPath:^=^^%"
set "delayedPath=%delayedPath:!=^!%"
for /f "delims=* tokens=1,2" %%A in ("%delayedPath%*%returnPath%") do (
  endlocal
  if "!!" equ "" (set "%~2=%%A" !) else set "%~2=%%B"
  exit /b 0
)

:noPath
>&2 echo Missing path argument - Use longPath /? for help.
exit /b 1

:notFound
>&2 echo Path not found
exit /b 1

Ответ 2

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

C:\PROGRA~2\Sophos\SOPHOS~2\SOPHOS~2.DLL

в

C:\Program Files (x86)\Sophos\Sophos Anti-Virus\sophos_detoured_x64.dll

batch4life