VBAで長いパスが扱えないと思ったら

Windowsでは長いパス(260文字前後)を使うと呪いに遭います

Windowsでは長いパスを使うと呪いに遭います。

(個人的には英語の長ったらしいディレクトリ名とファイルパスをついたものをJenkinsでこれまた長ったらしいワークスペースフォルダにチェックアウトした時に遭遇している)

こんなディレクトリとファイルがあったとしましょう。

C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\
    01234567.xlsx
    a.xlsx
    b.xlsx

上記中の 01234567.xlsxWindowsで許されている最長のファイルパスを持つエントリです。

ただ、ファイルパスが長過ぎるためか、、、

  • ExcelVBAのWorkbooks.Open()で開くことができません。(アプリケーション側から開くのも不可能です。)
  • FSOが妙な挙動を示すようになります。

このFSOの妙な挙動というのは、詳しくいうと、下記のようにファイルの列挙に失敗します。

Sub listFiles()

  Dim fso As New FileSystemObject

  Dim i&

  ' 正しいファイル数 3 が表示される
  Debug.Print fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files.Count

  ' 長いファイルパスのFileオブジェクトの作成にFor Each文で失敗し、2が表示される
  For Each f In fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files
    i& = i& + 1
  Next
  Debug.Print i

End Sub

ロングパス対策

では、どうやってこれを克服すればいいかというと、解決方法は簡単で、長いパス部分についてショートパスを使ってあげればOKです。

Sub listFiles2()

  Dim fso As New FileSystemObject
  Dim i&

  Debug.Print fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files.Count
  For Each f In fso.GetFolder("C:\LONGPA~1\012345~2").Files
    Debug.Print f
    i& = i& + 1
  Next
  Debug.Print i

End Sub

同様に、長過ぎるパスのブックをExcelは開けないので、かわりにショートパスを与えてあげれば問題なく開くことができます。

Dim wb As Workbook

' 失敗する
Set wb = Workbooks.Open("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\01234567.xlsx")

' これならOK
Set wb = Workbooks.Open("C:\LONGPA~1\012345~2\012345~1.XLS")

ちなみに上記のようなショートパスでファイルを開いた場合、ショートパスのファイルとしてVBA内では扱われます。

Debug.Print wb.Name ' 01234567.xlsx
Debug.Print wb.Path ' C:\LONGPA~1\012345~2
Debug.Print wb.FullName ' C:\LONGPA~1\012345~2\01234567.xlsx

ショートパス⇔ロングパス変換

たとえばファイルパスの一覧を出すといったマクロの場合、長過ぎるパスを処理する場合に、ショートパスが欲しかったり、逆にショートパスで処理しているが故にレポート出力時ではちゃんとロングパスで出力したい場合があると思います。

ロングパス → ショートパス変換はFSOを使って行うこともできますが、

Dim fso As New FileSystemObject

Dim shortPath As String
shortPath = fso.GetFile("えらい長いパス").ShortPath

そもそも長すぎてGetFileする時点で失敗するかもしれないし、相互の変換については、専用のWindows APIがあるので、それを使いましょう。

以下がWindows API(の宣言とその応用のためのVBAプロシージャ)です。64bit Officeでしか試していませんし素人なので、なんか間違ってるかも。(いつか調べます)

' LongPath -> ShortPath
#If VBA7 Then
Private Declare PtrSafe Function Win32APIGetShortPathName Lib "kernel32" Alias "GetShortPathNameA" _
  (ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal cchBuffer As LongLong) As LongLong
#Else
Private Declare Function Win32APIGetShortPathName Lib "kernel32" Alias "GetShortPathNameA" _
  (ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal cchBuffer As Long) As Long
#End If

Function GetShortPathName(ByVal longPath As String) As String
  Const PATH_LENGTH = 260

  Dim pathBuffer As String
  pathBuffer = String$(PATH_LENGTH + 1, vbNull)

  #If VBA7 Then
    Dim pathLength As LongLong
  #Else
    Dim pathLength As Long
  #End If
  pathLength = Win32APIGetShortPathName(longPath, pathBuffer, PATH_LENGTH)

  GetShortPathName = Left(pathBuffer, CLng(pathLength))
End Function


' ShortPath -> LongPath
#If VBA7 Then
Private Declare PtrSafe Function Win32APIGetLongPathName Lib "kernel32" Alias "GetLongPathNameA" _
  (ByVal lpszShortPath As String, ByVal lpszLongPath As String, ByVal cchBuffer As LongLong) As LongLong
#Else
Private Declare Function Win32APIGetLongPathName Lib "kernel32" Alias "GetLongPathNameA" _
  (ByVal lpszShortPath As String, ByVal lpszLongPath As String, ByVal cchBuffer As Long) As Long
#End If

Function GetLongPathName(ByVal shortPath As String) As String
  Const PATH_LENGTH = 260

  Dim pathBuffer As String
  pathBuffer = String$(PATH_LENGTH + 1, vbNull)

  #If VBA7 Then
    Dim pathLength As LongLong
  #Else
    Dim pathLength As Long
  #End If
  pathLength = Win32APIGetLongPathName(shortPath, pathBuffer, PATH_LENGTH)

  GetLongPathName = Left(pathBuffer, CLng(pathLength))

End Function

上記のソースで使っているWinAPIはこちら。

https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx https://msdn.microsoft.com/en-us/library/windows/desktop/aa364980(v=vs.85).aspx

このVBAソースはGistにもあげてあります。

https://gist.github.com/knjname/ba3b7d433655d5930a08