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

Я пишу пакетный скрипт Windows, и у меня есть аргумент или переменная, содержащая путь, который использует короткие имена 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, также допустимо.

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

4 ответа

Решение

Сначала я покажу, как преобразовать аргумент командного файла %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\*"находится по адресу /questions/39607869/kak-proverit-yavlyaetsya-li-fajl-katalogom-v-paketnom-skripte/39607889#39607889.

Но также можно использовать переменную FOR %%~aF модификатор атрибута для поиска d (каталог), находится по адресу /questions/39607869/kak-proverit-yavlyaetsya-li-fajl-katalogom-v-paketnom-skripte/39607877#39607877 и /questions/43059315/kak-proverit-yavlyaetsya-li-put-fajlom-ili-katalogom-v-paketnom-fajle-windows/43059325#43059325.

Так что вот полностью рабочий пакетный раствор

@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

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

Еще одна трудность возникает из-за того, что, когда имя пути не существует, 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. Это позволило бы избежать такой акробатики. В идеальном мире...

longPathUsingPython.bat

      @echo off
set SFN=%1
for /F %%i in ('python -c
  "import sys;from os.path import realpath;print(realpath(sys.argv[1]))"
    %SFN%') do set LFN=%%i
echo ShortFileName: %SFN%
echo LongFileName : %LFN%

Пример использования:

      (base) C:\VldTest1\cmd_exe>longPathUsingPython C:\PRAB99~1\OCTAVE~1.0\usr\bin\python.exe
ShortFileName: C:\PRAB99~1\OCTAVE~1.0\usr\bin\python.exe
LongFileName : C:\ProgramFilesVld_x64\Octave-8.1.0\usr\bin\python.exe

Для Python используйте:

      from os import realname

long_name = realname(short_name)
Другие вопросы по тегам