如何在批处理脚本中将Windows短路径转换为长路径名

16
我正在编写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也是可接受的。
注意:我在此问题的答案中发布了自己的答案。我搜索了网络,惊讶地发现这方面的信息很少。我开发了多个可行的解决方案,并认为其他人可能对我所发现的内容感兴趣。
4个回答

20

首先,我将演示如何转换批处理文件参数%1并将结果打印到屏幕上。

PowerShell

最简单的解决方案是使用PowerShell。我在Sergey Babkin的MSDN博客中找到了以下代码:

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

将该代码放入批处理脚本并打印结果非常简单:

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

然而,我尽量避免在批处理中使用PowerShell,有两个原因:

  • PowerShell不是XP的本地应用程序
  • 启动PowerShell的时间相当长,这使得批处理混合体相对较慢


CSCRIPT(JScript或VBS)

我在Computer Hope论坛上找到了这个VBS片段,它使用一个虚拟快捷方式来将短格式转换为长格式。

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);


Pure Batch - 纯批处理

令人惊讶的是,我的网络搜索未能找到纯批处理的解决方案。所以我只能自己动手。

如果你知道路径代表一个文件,那么将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的方法会错误地将具有符号链接或连接点的路径中的文件报告为文件夹,而这在公司网络环境中很常见。

我选择使用在https://dev59.com/unVC5IYBdhLWcg3w9F89#1466528找到的IF EXIST "yourPath\*"

但也可以使用FOR变量%%~aF属性修饰符来查找d(directory)属性,该方法在https://dev59.com/unVC5IYBdhLWcg3w9F89#3728742https://dev59.com/tWoy5IYBdhLWcg3wQr5i#8669636中有介绍。

所以这里是一个完全可行的纯批处理解决方案。

@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 命令会将带引号的 “^” 字符加倍。除了使用变量而不是字符串文字按引用传递值之外,没有好的解决方案。如果输入路径仅使用短名称,则不会有问题。但是,如果路径同时使用短名称和长名称,则可能存在问题。

  • 在批处理参数中传递“%”字面量可能会很棘手。很容易混淆是否需要将其加倍(如果有的话)。同样,通过变量在引用中传递值可能更容易。

  • 调用者可能从 FOR 循环中调用实用程序。如果变量或参数包含“%”,则实用程序中循环内扩展“%var%”或“%1”可能会导致无意中扩展 FOR 变量,因为 FOR 变量具有全局范围。实用程序不能在 FOR 循环中扩展参数,并且仅当使用延迟扩展时才能安全扩展变量。

  • 启用延迟扩展后,包含“!”变量的 FOR 变量的扩展将被破坏。

  • 调用环境可能启用或禁用延迟扩展。将包含“!”和“^”的值传递到延迟扩展环境中的 ENDLOCAL 屏障需要将带引号的“!”转义为“^!”。此外,只有在行包含“!”时,带引号的“^”才必须转义为“^^”。当然,如果调用环境已禁用延迟扩展,则不应转义这些字符。

我开发了两种健壮的实用程序形式,分别是 JScript 和纯批处理解决方案,考虑到上述所有边缘情况。

默认情况下,实用程序期望路径作为字符串文字,但如果使用 /V 选项,则接受包含路径的变量名称。

默认情况下,实用程序仅将结果打印到 stdout。但是,如果将返回变量的名称作为额外参数传递,则可以将结果返回到变量中。无论您的 CALLing 环境启用还是禁用延迟扩展,都保证返回正确的值。

完整的文档已经嵌入到实用程序中,可以使用 /?选项进行访问。

有一些晦涩的限制:

  • 返回变量名不得包含%字符
  • 同样,/V选项输入变量名不得包含%字符。
  • 输入路径不能包含内部双引号。路径可以被封装在一组双引号中,但是不能有其他引号。

我没有测试过实用程序是否能够处理路径名中的Unicode,或者它们是否可以处理UNC路径。


jLongPath.bat - 混合JScript / batch

@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

从未遇到过这个问题,但知道有解决方案还是很好的,谢谢! - SachaDee
我猜longPath.bat的第31行应该写成if not defined !%~2!,是吗? - aschipfl
1
@aschipfl - 不,这种奇怪的语法是有意的。当%~2未定义时,我需要IF语句具有有效的语法。 !!在解析该行时存在,因此IF条件始终有效。然后在执行时,!!变成空白,因为延迟扩展已启用。 - dbenham
我在使用这个脚本将完全由短路径组成的Windows PATH变量转换为长路径名时遇到了问题。我已经想出了以下命令:START "" /B CMD /E:ON /V:ON /Q /D /C "FOR %G IN (%PATH%) DO CALL C:\Windows\System32\LongPath.bat %G",可以在cmd.exe中运行,并以所需的长名称格式正确输出80%的PATH短名称。然而,一些文件路径仍然以短名称形式留下,我无法弄清楚原因。有人有任何想法吗? - slyfox1186

2
面对同样的问题,我在纯批处理中使用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. 我希望微软能够在FOR命令中添加一个%~l修改器,它可以完全相反于%~s修改器。这将避免进行这样的杂技表演。在理想的世界中...


0

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

-1

对于Python,使用:

from os import realname

long_name = realname(short_name)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接