VBAで長いパスが扱えないと思ったら
Windowsでは長いパス(260文字前後)を使うと呪いに遭います
Windowsでは長いパスを使うと呪いに遭います。
(個人的には英語の長ったらしいディレクトリ名とファイルパスをついたものをJenkinsでこれまた長ったらしいワークスペースフォルダにチェックアウトした時に遭遇している)
こんなディレクトリとファイルがあったとしましょう。
C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\ 01234567.xlsx a.xlsx b.xlsx
上記中の 01234567.xlsx
はWindowsで許されている最長のファイルパスを持つエントリです。
ただ、ファイルパスが長過ぎるためか、、、
- 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にもあげてあります。