8.3, the Undead Path

8.3 is a common reference to the DOS name limit of 8 characters for the filename and 3 characters for the file extension.  E.g.: command.com.

NTFS did away with that, but it still haunts us in the form of legacy .BAT files that can’t handle file paths with space characters in them (and may blow chunks on other ‘funny’ characters such as parentheses and such.  I didn’t do a lot with MS-DOS, and I named them sensible names.

In this case, I uncovered a PSH wrapper to a .WSH (Windows Scripting Host) to a .BAT file.  Actually, a huge self-contained microcosm of interdependent .BAT files.  All in a build system that’s called nightly, interfaces with a gazillion endpoints via osql.exe and other VB files, etc.

It’s blowing up because the someone checked a file into our version control system with a space in it.  Well, rather than fight that battle, I figured it was a good time to learn how NTFS maps 8.3 names.

It turns out the following lines are the magic:

     $fso = New-Object -ComObject Scripting.FileSystemObject;
    $shortPath = $fso.GetFile($Path).ShortPath;

However, there are two key limitations to keep in mind:

- This only works for files, not folders.

- This doesn’t work for \\unc\paths.

We can’t get around the UNC path limitation, but we can hack around the file-only one by creating a temp file in the destination, then getting its ShortPath, and truncating the filename portion of the resulting path out.  (NTFS can use short paths to folders.  It’s Scripting.FileSystemObject that’s the roadblock.)

Come to think of it, this might be a way to address another issue someone on my team reported: the 260 character file path limit.  Hmmm…

 ########################################
function Get-Location83
########################################
{
    <#
    .synopsis
    Return DOS style 8.3 name

    .description
    Some legacy commands won't accept a space in the name, no matter how you try to quote or escape it.  If it's a drive, this will return the 8.3 name.  If it isn't a drive (example, a \\unc\path), this will fail.

    .parameter Path
    Path to convert to 8.3 name

    .parameter Force
    Script stops if -Path value is not found.  This attempts to create it as a folder.

    #>

    param (
        [string]$Path = $((Get-Location).Path),
        [switch]$Force
    );

    #region sanity checks

    # try to make sure $Path exists  
    if (!(Test-Path -Path $Path))
    {
    
        if ($Force)
        {
    
            $message = "$($MyInvocation.MyCommand.Name) -Path '$Path' not found, and -Force specified.  Creating as directory.";
            Write-Warning -Message $message;
            New-Item -ItemType Directory -Path $Path -ErrorAction SilentlyContinue | Out-Null;

        } # if ($Force)
        else
        {
    
            $message = "$($MyInvocation.MyCommand.Name) -Path '$Path' not found, and -Force not specified.  Stopping.";
            Write-Warning -Message $message;
            Write-Error -ErrorAction SilentlyContinue -Message $message;
            return;
        
        } # if ($Force) ... else

    } # if (!(Test-Path -Path $Path))

    # really make sure $Path exists
    if (!(Test-Path -Path $Path))
    {
    
        $message = "$($MyInvocation.MyCommand.Name) -Path '$Path' not found, and -Force could not create it.  Stopping.";
        Write-Warning -Message $message;
        Write-Error -ErrorAction SilentlyContinue -Message $message;
        return;
     
    } # if (!(Test-Path -Path $Path))

    $Path = (Resolve-Path -Path $Path).Path;


    # check if this is a disk drive.  \\unc\paths and other funky PS resources exposed as drives don't work with this.
    $drive = $Path -replace ':.*';
    $drives = (Get-PSProvider -PSProvider FileSystem).Drives | 
    % {
    
        $_.Name;
    
    } # $drives = (Get-PSProvider -PSProvider FileSystem).Drives | 

    if ([array]::IndexOf($drives, $drive) -eq -1)
    {
    
        $message = "$($MyInvocation.MyCommand.Name) -Path '$Path' is not a disk drive.";
        Write-Warning -Message $message;
        New-Item -ItemType Directory -Path $Path -ErrorAction SilentlyContinue | Out-Null;

    } # if ([array]::IndexOf($drives, $drive) -eq -1)

    #endregion
    #region processing

    $item = Get-Item $Path;

    if ($item.GetType().Name -eq 'DirectoryInfo')
    { # if $Path is a folder
    
        if ($file = Get-ChildItem -Path $Path | 
            ? { $_.GetType().Name -eq 'FileInfo' } |
            Select-Object -First 1
        )
        { # if a file under this folder exists
          
            $Path = $file.FullName;

        } # if ($file = Get-ChildItem -Path $Path | 
        else
        { # no files exist.  create one
          
            $guid = [guid]::NewGuid().Guid;
            $Path = "$Path\$guid.txt";

            New-Item -ItemType File -Path $Path -ErrorAction SilentlyContinue | Out-Null;

            if (!(Test-Path -Path $Path))
            {
    
                $message = "$($MyInvocation.MyCommand.Name) -Path '$Path' could not create temporary file.  Stopping.";
                Write-Warning -Message $message;
                Write-Error -ErrorAction SilentlyContinue -Message $message;
                return;
     
            } # if (!(Test-Path -Path $Path))

        } # if ($file = Get-ChildItem -Path $Path |  ... else

    } # if ($item.GetType().Name -eq 'DirectoryInfo')

    $fso = New-Object -ComObject Scripting.FileSystemObject;
    $shortPath = $fso.GetFile($Path).ShortPath;

    #endregion
    #region clean up after self if $Path was initially a folder.

    if ($item.GetType().Name -eq 'DirectoryInfo')
    {

        $shortPath = $shortPath -replace '\\[^\\]*$';
        
        if ($guid)
        {

            Remove-Item -Path $Path;
        
        } # if ($guid)

    } # if ($item.GetType().Name -eq 'DirectoryInfo')

    #endregion
    #region output data

    $shortPath;

    #endregion

} # function Get-Location83