Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I convert a Windows short name path into long names within a batch script

I am writing a Windows batch script, and I have an argument or variable containing a path that uses short 8.3 names. The path might represent a file or folder.

How can I convert the 8.3 path into a long-name path? At a minimum I would like to be able to simply print out the full long-name path. But ideally I would like to safely get the long-name path into a new variable.

For example, given the path C:\PROGRA~1\AVASTS~1\avast\BROWSE~1.INI, I would like to return C:\Program Files\AVAST Software\avast\BrowserCleanup.ini.

As a batch enthusiast, I am most interested in a pure batch solution that uses only native Windows commands. But hybird use of other native scripting tools like PowerShell and JScript is also acceptable.

Note: I am posting my own answer for this question. I searched the web, and was surprised to find precious little on this subject. I developed multiple working strategies, and thought others might be interested in what I found.

like image 371
dbenham Avatar asked Dec 26 '15 18:12

dbenham


People also ask

How to write filename with space in cmd?

Use quotation marks when specifying long filenames or paths with spaces. For example, typing the copy c:\my file name d:\my new file name command at the command prompt results in the following error message: The system cannot find the file specified. The quotation marks must be used.

Does NTFS support long file names?

NTFS file system as the first or only character in the filename, although NTFS (and many command-line tools) do support this. A long file name (LFN) can be up to 255 characters long. NTFS supports paths up to 32768 characters in length, but only when using the Unicode APIs.


1 Answers

First I will demonstrate how to convert a batch file argument %1 and print the result to the screen.

PowerShell

The simplest solution is to use PowerShell. I found the following code on a MSDN blog by Sergey Babkin

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

Putting that code in a batch script and printing the result is trivial:

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

However, I try to avoid using PowerShell within batch for two reasons

  • PowerShell is not native to XP
  • The start up time for PowerShell is considerable, so it makes the batch hybrid relatively slow


CSCRIPT (JScript or VBS)

I found this VBS snippet at Computer Hope forum that uses a dummy shortcut to convert from short to long form.

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

I found similar code at a Microsoft newsgroup archive and an old vbscript forum.

The code only supports file paths, and it is a bit easier to embed JScript within batch. After converting to JScript and adding an exception handler to get a folder if a file fails, I get the following hybrid code:

@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

Surprisingly, my web search failed to find a pure batch solution. So I was on my own.

If you know that the path represents a file, then it is a simple matter to convert the 8.3 file name into a long name using dir /b "yourFilePath". However, that does not resolve the names of the parent folder(s).

The situation is even worse if the path represents a folder. There is no way to list a specific folder using only the DIR command - it always lists the contents of the folder instead of the folder name itself.

I tried a number of strategies to handle the folder paths, and none of them worked:

  • CD or PUSHD to the path and then look at the prompt - it preserves the short folder names
  • XCOPY with /L and /F options - it also preserves the short folder names
  • Argument or FOR variable modifier %~f1 or %%~fA - preserves the short names
  • FORFILES - doesn't appear to support short names.

The only solution I was able to come up with was to use DIR to iteratively convert each folder within the path, one at a time. This requires that I use DIR /X /B /AD to list all folders in the parent folder, including their 8.3 names, and then use FINDSTR to locate the correct short folder name. I rely on the fact that the short file name always appears in the exact same place after the <DIR> text. Once I locate the correct line, I can use variable substring or find/replace operations, or FOR /F to parse out the long folder name. I opted to use FOR /F.

One other stumbling block I had was to determine if the original path represents a file or a folder. The frequently used approach of appending a backslash and using IF EXIST "yourPath\" echo FOLDER improperly reports a file as a folder if the path involves a symbolic link or junction, which is common in company network environments.

I opted to use IF EXIST "yourPath\*", found at https://stackoverflow.com/a/1466528/1012053.

But it is also possible to use the FOR variable %%~aF attribute modifier to look for the d (directory) attribute, found at https://stackoverflow.com/a/3728742/1012053, and https://stackoverflow.com/a/8669636/1012053.

So here is a fully working pure batch solution

@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

The GOTO used to iterate the individual folders will slow the operation down if there are many folders. If I really wanted to optimize for speed, I could use FOR /F to invoke another batch process, and resolve each folder in an infinite FOR /L %%N IN () DO... loop, and use EXIT to break out of the loop once I reach the root. But I did not bother.


Devoloping robust utilities that can return the result in a variable

There are a number of edge cases that can complicate development of a robust script given that ^, %, and ! are all legal characters in file/folder names.

  • CALL doubles quoted ^ characters. There is no good solution to this problem, other than to pass the value by reference using a variable instead of as a string literal. This is not an issue if the input path uses only short names. But it could be an issue if the path uses a mixture of short and long names.

  • Passing % literals within batch arguments can be tricky. It can get confusing as to who many times (if at all) it should be doubled. Again it might be easier to pass the value by reference within a variable.

  • The CALLer may call the utility from within a FOR loop. If a variable or argument contains %, then expansion of %var% or %1 within a loop in the utility can lead to inadvertent FOR variable exansion because the FOR variables are global in scope. The utility must not expand arguments within a FOR loop, and variables can only be safely expanded within a FOR loop if delayed expansion is used.

  • Expansion of FOR variables containing ! will be corrupted if delayed expansion is enabled.

  • The CALLing environment may have delayed expansion enabled or disabled. Passing values containing ! and ^ across the ENDLOCAL barrier to a delayed expansion environment requires that quoted ! be escaped as ^!. Also, quoted ^ must be escaped as ^^, but only if the line contains !. Of course those characters should not be escaped if the CALLing environment has delayed expansion disabled.

I have developed robust utility forms of both the JScript and pure batch solutions that take into account all of the edge cases above.

The utilities expect the path as a string literal by default, but accept a variable name that contains the path if the /V option is used.

By default the utilties simply print the result to stdout. But the result can be returned in a variable if you pass the name of the return variable as an extra argument. The correct value is guaranteed to be returned, regardless whether delayed expansion is enabled or disabled in your CALLing environment.

Full documentation is embedded within the utilities, and can be accessed using the /? option.

There are a few obscure limitations:

  • The return variable name must not contain ! or % characters
  • Likewise /V option input variable name must not contain ! or % characters.
  • The input path must not contain internal double quotes. It is OK for the path to be enclosed witin one set of double quotes, but there should not be any additional quotes within.

I have not tested whether the utilities work with unicode in path names, or if they work with UNC paths.


jLongPath.bat - hybrid 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 - Pure batch

:::
:::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
like image 98
dbenham Avatar answered Oct 21 '22 00:10

dbenham