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

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

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

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

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

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


person dbenham    schedule 26.12.2015    source источник


Ответы (2)


Сначала я покажу, как преобразовать аргумент пакетного файла %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\*", найденный по адресу https://stackoverflow.com/a/1466528/1012053.

Но также можно использовать модификатор атрибута переменной FOR %%~aF для поиска атрибута d (каталог), который можно найти по адресу https://stackoverflow.com/a/3728742/1012053 и https://stackoverflow.com/a/8669636/1012053.

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

@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 может быть включено или отключено отложенное расширение. Передача значений, содержащих ! и ^, через барьер ENDLOCAL в среду с отложенным расширением требует, чтобы ! в кавычках экранировалось как ^!. Кроме того, ^ в кавычках необходимо экранировать как ^^, но только если строка содержит !. Конечно, эти символы не следует экранировать, если в среде 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
person dbenham    schedule 26.12.2015
comment
Никогда не было этой проблемы, но приятно знать, что есть некоторые решения, спасибо! - person SachaDee; 26.12.2015
comment
Я предполагаю, что строка 31 из longPath.bat должна читаться как if not defined !%~2!, не так ли? - person aschipfl; 28.12.2015
comment
@aschipfl - Нет, странный синтаксис сделан намеренно. Мне нужно, чтобы оператор IF имел допустимый синтаксис, когда %~2 не определено. !! присутствует, когда строка анализируется, поэтому условие IF действует независимо. Затем при выполнении !! становится ничем, потому что включено отложенное расширение. - person dbenham; 29.12.2015

Столкнувшись с той же проблемой, я нашел более простое решение в чистом пакетном режиме.
Он использует команду ATTRIB, которую вы, видимо, не пробовали.
attrib.exe выводит длинное имя файла, даже при получении короткого файла name в качестве аргумента.
Но, к сожалению, он также не расширяет короткие имена в пути.
Как и в вашем решении, для получения полного длинного пути требуется зацикливание на всем пути.

Еще одна трудность возникает из-за того, что когда имя пути не существует, attrib.exe выводит сообщение об ошибке на стандартный вывод, заканчивающееся именем пути (точно так же, как при обычном выводе), и не возвращает уровень ошибки. Это можно обойти, отфильтровав выходные данные attrib.exe, чтобы удалить строки, содержащие - перед именем диска.

Тестовая программа для подпрограммы расширения :GetLongPathname:

@echo off

:# Convert a short or long pathname to a full long pathname
:GetLongPathname %1=PATHNAME %2=Output variable name
setlocal EnableDelayedExpansion
set "FULL_SHORT=%~fs1"           &:# Make sure it really is short all the way through
set "FULL_SHORT=%FULL_SHORT:~3%" &:# Remove the drive and initial \
set "FULL_LONG=%~d1"             &:# Begin with just the drive
if defined FULL_SHORT for %%x in ("!FULL_SHORT:\=" "!") do ( :# Loop on all short components
  set "ATTRIB_OUTPUT=" &:# If the file does not exist, filter-out attrib.exe error message on stdout, with its - before the drive.
  for /f "delims=" %%l in ('attrib "!FULL_LONG!\%%~x" 2^>NUL ^| findstr /v /c:" - %~d1"') do set "ATTRIB_OUTPUT=%%l"
  if defined ATTRIB_OUTPUT ( :# Extract the long name from the attrib.exe output
    for %%f in ("!ATTRIB_OUTPUT:*\=\!") do set "LONG_NAME=%%~nxf"
  ) else (                   :# Use the short name (which does not exist)
    set "LONG_NAME=%%~x"
  )
  set "FULL_LONG=!FULL_LONG!\!LONG_NAME!"
) else set "FULL_LONG=%~d1\"
endlocal & if not "%~2"=="" (set "%~2=%FULL_LONG%") else echo %FULL_LONG%
exit /b

Пример вывода:

C:\JFL\Temp>test "C:\Progra~1\Window~1"
C:\Program Files\Windows Defender

C:\JFL\Temp>test "\Progra~1\Not There\Not There Either"
C:\Program Files\Not There\Not There Either

C:\JFL\Temp>

PS. Я бы хотел, чтобы Microsoft добавила модификатор %~l к команде FOR, делая полную противоположность модификатору %~s. Это позволило бы избежать такой акробатики. В идеальном мире...

person Jean-François Larvoire    schedule 28.10.2020