Недавно, работая в VBA, я попытался переименовать группу файлов, расположенных в длинных, вложенных директориях. Неожиданно возникли ошибки, которые не позволяли это осуществить. Оказалось, что в Windows 10 (тем более в более ранних версиях) существуют ограничения на длину путей (см., к примеру https://learn.microsoft.com/ru-ru/windows/win32/fileio/maximum-file-path-limitation?tabs=registry). Решения, найденные в результате поиска не принесли результата. Да, для манипуляции с длинными путями необходимо разрешить их в реестре ( раздел Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled (Type: REG_DWORD) реестра должен существовать и иметь значение 1), но даже если они будут разрешены, манипулировать вы ими не можете, т.к. сам проводник Windows не позволяет работать с длинными путями. Возможно, скрипт VBA, при манипуляции с файлами использует проводник Windows. С другой стороны, с длинными путями хорошо работает проводник 7-Zip File Manager, при этом он имеется практически на каждом компьютере. Если это не так - его легко установить.

Возникла идея обойти ограничения Windows и использовать для манипуляции с файлами именно проводник 7-Zip File Manager. В результате получился рабочий скрипт, который позволяет производить перемещение и переименование файлов с длинными путями.

'Пример использования функции перемещения файлов с длинными путями
Public Sub Per_Files()

Dim sDir As String, dDir As String, old_name As String, new_name As String

   sDir = "C:\1\" 'Исходная папка"
   dDir = "C:\2\" 'Целевая папка
    
   old_name = "1.pdf" 'Копируемый файл: старое имя
   new_name = "1-1.pdf" 'Копируемый файл: новое имя

Call cp7z(sDir, dDir, old_name, new_name)

End Sub

'Подпрограмма перемещения файлов с длинными путями с помощью архиватора 7z
Public Sub cp7z(ByVal sDir As String, ByVal dDir As String, ByVal oldFile As String, ByVal nFile As String)

Dim PrDir As String, tDir As String, comstr As String

    PrDir = """C:\Program Files\7-Zip\7z.exe""" 'Расположение исполняемого файла 7z
    tDir = "C:\tmp\tmp.7z" 'Вспомогательная папка и файл
    
    'Проверка существования файла tmp.7z. Если такой файл есть - подбирается новое свободное имя
    Do While Dir(tDir) <> ""
        tDir = "C:\tmp\" & WorksheetFunction.RandBetween(1, 1000) & "tmp.7z"
'        MsgBox tDir
    Loop

    'Создание архива C:\tmp\*tmp.7z из исходного файла с длинным путём (без компрессии, т.е. копирование в файл *tmp.7z)
    comstr = PrDir & " a -mx0 " & tDir & " " & Chr(34) & sDir & oldFile & Chr(34)
    Debug.Print comstr
    ShellAndWait comstr
    
    'Переименование файла в архиве C:\tmp\*tmp.7z старое имя -> новое имя
    comstr = PrDir & " rn " & tDir & " " & oldFile & " " & nFile
    Debug.Print comstr
    ShellAndWait comstr

    'Копирование файла из архива C:\tmp\*tmp.7z в целевую папку
    comstr = PrDir & " e -y " & tDir & " -o" & Chr(34) & dDir & Chr(34)
    Debug.Print comstr
    ShellAndWait comstr

    'Удаление вспомогательного файла
    Kill tDir

End Sub

'Подпрограмма запуска процесса 7z с ожиданием завершения процесса
Sub ShellAndWait(pathFile As String)
Dim WshShell As Object
    Set WshShell = CreateObject("Wscript.Shell")
    WshShell.Run pathFile, 0, True 'Обязательно True, процесс должен завершиться,
  'иначе команды скрипта начнут выполняться раньше срока и скрипт не будет работать
End Sub

Как видно из скрипта, необходимый файл архивируется архиватором 7z во вспомогательную папку во временный файл (без сжатия), а затем распаковывается в файл по новому пути. Временный файл удаляется. При этом файл может быть переименован. В данном случае, в скрипте, в качестве исходных и целевых папок используются папки "C:\1\" и "C:\2\", но в вашем скрипте вы можете задавать пути (в виде переменных string) любой длины и вложенности.

Данный скрипт осуществляет копирование + переименование файлов. Вы можете реализовать функции перемещения, слегка модифицировав скрипт (добавив удаление исходного файла).

И не забывайте, что для функционирования данного скрипта, у вас на компьютере должен быть установлен архиватор 7z и в скрипте необходимо прописать правильный путь к исполняемому файлу архиватора (переменная PrDir).

Комментарии (13)


  1. fido_max
    24.10.2023 17:35
    +1

    А еще 8192 символ в cmd консольке пропадает. Причем строка не обрезается, а просто символ пропадает. https://stackoverflow.com/questions/2916865/how-to-get-around-the-command-line-length-limit


    1. Kutush Автор
      24.10.2023 17:35

      Но это по крайней мере не ограничение в 260 символов ). Я не думаю, что в практической деятельности встречаются пути длиной более 8000 символов...


  1. aamonster
    24.10.2023 17:35
    +2

    Так вы пути-то длинные в своём скрипте записывали в виде \?\...? Или 7zip только для этого и использовали (типа в скрипте обычная запись, а в эту извращённую пусть 7zip переводит)?


    1. Kutush Автор
      24.10.2023 17:35
      +1

      Пробовал, и много различных способов записи длинных путей - не получается при использовании стандартных функций манипуляции файлов VBA. Не позволяет.


  1. mesvobodnye
    24.10.2023 17:35

    Для перемещения таких файлов используем Тотал Коммандер


    1. Kutush Автор
      24.10.2023 17:35

      Допустимо, если это 2-3 файла. У меня была задача переименовать тысячи файлов в длинных вложенных папках с именами в которых содержалось полное название госта + допинфа (очень очень длинное) - то есть в ручную это сделать нереально.

      Может быть вы знаете как использовать тотал в VBA подобно вышеописанному способу 7z? Подскажите, можно ли из строки шела выполнять команды Тотала на копирование, перемещение и тп.


      1. Kutush Автор
        24.10.2023 17:35

        Поискал сегодня - на сайте разработчика есть раздел по работе с шелом: https://www.ghisler.ch/wiki/index.php?title=Command_line_parameters

        К сожалению, не имеется команд для манипуляции с файлами.


    1. VIGOTiraspol
      24.10.2023 17:35

      TC не работает с длинными именами.


  1. B13nerd
    24.10.2023 17:35
    +4

    1) Если VBA (не VBS), почему не использовать WinAPI CopyFile() с добавлением к пути "\\?\" ?

    2) Dim sDir, dDir, old_name, new_name As String - типичная ошибка (так сказать). sDir, dDir, old_name будут описаны как Variant.


    1. Kutush Автор
      24.10.2023 17:35

      Пробовал. У меня не получилось. Предложите рабочий вариант.


    1. Kutush Автор
      24.10.2023 17:35

      sDir, dDir, old_name будут описаны как Variant

      Действительно, спасибо, поправил скрипт.


  1. belch84
    24.10.2023 17:35
    +1

    Когда-то очень давно решал в чем-то похожую задачу для FoxPro for DOS. Необходимо было обеспечить работу с файлами, имеющими длинные имена для приложения, которое понимало только короткие имена файлов в формате 8.3 (максимум 8 символов на имя и 3 на расширение имени, для имен папок тоже). Для совместимости в файловых системах тех времен (FAT32 или NTFS) поддерживались одновременно длинное и короткое имена файла (не знаю, как обстоит дело сейчас). Идея состояла в том, чтобы определять короткое имя по длинному и наоборот при помощи MS-DOS команды DIR. При соответствующих параметрах эта команда для заданного файла выдает его длинное и короткое имена, которые можно записать во временный файл, а затем извлечь из него

    Код программы на языке FoxPro (очень похожем на BASIC)
    * set talk off
    * S = fnm_cnv('H:\Накладные_вход', .T.)
    * S = fnm_cnv('H:\_1905~1')
    * ?S
    * fnm_cnv('C:\USER1\EXP_DPA\temp3dasjdajsd\',.T.)
    
    FUNCTION fnm_cnv
    PARAMETER m.fnm, m.src_long
    * преобразование длинных имен файлов в короткие и наоборот
      PRIVATE S, is_long
      
      is_long = type('m.src_long') = 'L' and m.src_long
      IF getenv('OS') = 'Windows_NT'
        S = getnm_NT(m.fnm, m.is_long)
      ELSE
        S = getnm_98(m.fnm, m.is_long)
      ENDIF
    RETURN S
    
    FUNCTION getnm_NT
    PARAMETER m.fnm, m.src_long
    * преобразование длинного имени файла в короткое или наоборот
    * fnm       - исходное имя файла
    * src_long - ИСТИНА для преобразования длинного в короткое,
    *            не задано или ЛОЖЬ для обратного пр-ния
    * Исходное имя должно быть задано с полным путем!
      
      IF type('UDir') # 'C'
        PRIVATE UDir
        UDir = ''
      ENDIF
      
      PRIVATE src_nm, tmp_nm, bat_nm, h, I, S, p1, ;
              p2, OS_NT, cur_dir, cur_nm, long_nm, short_nm, drv, ;
              is_dir, success, long2short, result
      tmp_nm = UDir + '__tmp__.txt'
      bat_nm = UDir + 'longdir.bat'
      result = ''
      success = .F.
      
      OS_NT = (getenv('OS') = 'Windows_NT')
      long2short = type('m.src_long') = 'L' and m.src_long
      
      DIMENSION tmp_arr[1]
      
      IF not m.OS_NT
        WAIT WINDOW 'GETNM_NT: неверная версия операционной системы'
        RETURN m.result
      ENDIF
      
      src_nm = alltrim(m.fnm)
      IF at(':\', m.src_nm) = 0
        WAIT WINDOW 'GETNM_NT: неверный полный путь для файла' + chr(13) + ;
                    m.src_nm
        RETURN m.result
      ENDIF
      drv = left(m.src_nm,3)
      
      IF not m.long2short and not file(m.src_nm)
        result = ''
        RETURN m.result
      ENDIF
      
      * обрабатываем полный путь к файлу последовательно
      FOR I = 1 TO 999
      
        * определяем, является ли текущий элемент именем 
        * оглавления или файла
        p1 = at('\',m.src_nm, I+1)
        is_dir = (p1 > 0)
        
        * выделяем для текущего элемента его имя и имя оглавления,
        * в котором он содержится
        p2 = at('\', m.src_nm, I)
        cur_dir = left(m.src_nm, p2)
        IF m.is_dir
          cur_nm = substr(m.src_nm,p2+1,p1-p2-1)
        ELSE
          cur_nm = substr(m.src_nm, p2 + 1)
        ENDIF
        
        * с помощью команды DIR формируем текстовый файл,
        * в котором содержатся длинное и короткое имена тек.элемента
      
        * создаем временный BAT-файл для запуска
        ERASE (bat_nm)
        h = fcreate(bat_nm)
        = fputs(h, '@echo off')
        IF m.is_dir
          = fputs(h, 'DIR ' + m.cur_dir + '*.*' + ' /X /D >' + m.tmp_nm)
        ELSE 
          = fputs(h, 'DIR ' + m.cur_dir + ' /X >' + m.tmp_nm)
        ENDIF
        = fclose(h)
      
        * выполняем BAT-файл и формирем текстовый файл с результатом DIR
        ERASE (m.tmp_nm)
        RUN cmd /c &bat_nm
        
        IF not file(m.tmp_nm)
          WAIT WINDOW 'Ошибка при создании файла, содержащего длинные имена'
          result = ''
          RETURN m.result
        ENDIF
        
        * удаляем BAT-файл
        ERASE (bat_nm)
        
        * открываем файл с результатми для анализа
        h = fopen(m.tmp_nm)
        
        * если первая строка пустая - пропускаем ее
        S = fgets(h,512)
        IF empty(S)
          S = fgets(h,512)
        ENDIF
        
        * если в текущей строке инф. о томе - пропускаем ее
        IF not feof(h) and ;
           ('том в устройстве' $ lower(S) or 'volume in drive' $ lower(S))
          S = fgets(h,512)
        ENDIF
        
        * если в текущей строке серийный номер тома - пропускаем ее
        IF not feof(h) and ;
           ('серийный номер тома' $ lower(S) or 'volume serial number' $ lower(S))
          S = fgets(h,512)
        ENDIF
        
        * если текущая строка пустая - пропускаем ее
        IF empty(S)
          S = fgets(h,512)
        ENDIF
        
        * пропускаем имя текущего оглавления
        IF not feof(h) 
          S = fgets(h,512)
        ENDIF
    
        * отыскиваем текущий элемент в файле с результатами DIR и 
        * преобразуем его нужным способом
        success = .F.
        DO WHILE not feof(h) and not success
          S = fgets(h, 512)
          IF empty(left(S,12)) or empty(substr(S,50))
            LOOP
          ENDIF
        
          short_nm = trim(substr(S,37,12))
          long_nm = trim(substr(S, 50))
          
          IF not m.long2short
            success = (upper(m.cur_nm) == upper(m.long_nm)) or ;
                      (upper(m.cur_nm) == upper(m.short_nm))
          ELSE
            * длинное имя в короткое
            success = (upper(m.cur_nm) == upper(m.long_nm))
          ENDIF
        ENDDO
      
        = fclose(h)
        ERASE (m.tmp_nm)
      
        IF not m.success
          EXIT
        ENDIF
        
        IF not m.long2short
          result = m.result + m.long_nm
        ELSE
          IF not empty(m.short_nm)
            result = m.result + m.short_nm
          ELSE
            result = m.result + m.long_nm
          ENDIF
        ENDIF
        
        IF m.is_dir
          result = m.result + '\'
        ENDIF
        
        IF not m.is_dir
          EXIT
        ENDIF
          
      ENDFOR
    
      IF m.success and not empty(m.result)
        result = m.drv + m.result
      ENDIF
     
    RETURN m.result
    
    FUNCTION getnm_98
    PARAMETER m.fnm, m.src_long
    * преобразование длинного имени файла в короткое или наоборот
    * fnm       - исходное имя файла
    * src_long - ИСТИНА для преобразования длинного в короткое,
    *            не задано или ЛОЖЬ для обратного пр-ния
    * Исходное имя должно быть задано с полным путем!
      
      IF type('UDir') # 'C'
        PRIVATE UDir
        UDir = ''
      ENDIF
      
      PRIVATE src_nm, tmp_nm, bat_nm, h, I, S, p1, ;
              p2, OS_NT, cur_dir, cur_nm, long_nm, short_nm, drv, ;
              is_dir, success, long2short, result
      tmp_nm = UDir + '__tmp__.txt'
      bat_nm = UDir + 'longdir.bat'
      result = ''
      success = .F.
      
      OS_NT = (getenv('OS') = 'Windows_NT')
      long2short = type('m.src_long') = 'L' and m.src_long
      
      DIMENSION tmp_arr[1]
      
      IF m.OS_NT
        WAIT WINDOW 'GETNM_98: неверная версия операционной системы'
        RETURN m.result
      ENDIF
      
      src_nm = alltrim(m.fnm)
      IF at(':\', m.src_nm) = 0
        WAIT WINDOW 'GETNM_98: неверный полный путь для файла' + chr(13) + ;
                    m.src_nm
        RETURN m.result
      ENDIF
      drv = left(m.src_nm,3)
      
      IF not m.long2short and not file(m.src_nm)
        result = ''
        RETURN m.result
      ENDIF
      
      * обрабатываем полный путь к файлу последовательно
      FOR I = 1 TO 999
      
        * определяем, является ли текущий элемент именем 
        * оглавления или файла
        p1 = at('\',m.src_nm, I+1)
        is_dir = (p1 > 0)
        
        * выделяем для текущего элемента его имя и имя оглавления,
        * в котором он содержится
        p2 = at('\', m.src_nm, I)
        cur_dir = left(m.src_nm, p2)
        IF m.is_dir
          cur_nm = substr(m.src_nm,p2+1,p1-p2-1)
        ELSE
          cur_nm = substr(m.src_nm, p2 + 1)
        ENDIF
        
        * с помощью команды DIR формируем текстовый файл,
        * в котором содержатся длинное и короткое имена тек.элемента
      
        * создаем временный BAT-файл для запуска
        ERASE (bat_nm)
        h = fcreate(bat_nm)
        = fputs(h, '@echo off')
        IF m.is_dir
          = fputs(h, 'DIR "' + m.cur_dir + '*.*' + '" >' + m.tmp_nm)
        ELSE 
          = fputs(h, 'DIR "' + m.cur_dir + '" >' + m.tmp_nm)
        ENDIF
        = fclose(h)
      
        * выполняем BAT-файл и формирем текстовый файл с результатом DIR
        RUN &bat_nm
        
        IF not file(m.tmp_nm)
          WAIT WINDOW 'Ошибка при создании файла, содержащего длинные имена'
          RETURN 0
        ENDIF
        
        * удаляем BAT-файл
        ERASE (bat_nm)
        
        * открываем файл с результатми для анализа
        h = fopen(m.tmp_nm)
        
        * если первая строка пустая - пропускаем ее
        S = fgets(h,512)
        IF empty(S)
          S = fgets(h,512)
        ENDIF
        
        * если в текущей строке инф. о томе - пропускаем ее
        IF not feof(h) and ;
           ('том в устройстве' $ lower(S) or 'volume in drive' $ lower(S))
          S = fgets(h,512)
        ENDIF
        
        * если в текущей строке серийный номер тома - пропускаем ее
        IF not feof(h) and ;
           ('серийный номер тома' $ lower(S) or 'volume serial number' $ lower(S))
          S = fgets(h,512)
        ENDIF
        
        * если текущая строка пустая - пропускаем ее
        IF empty(S)
          S = fgets(h,512)
        ENDIF
        
        * пропускаем имя текущего оглавления
        IF not feof(h) 
          S = fgets(h,512)
        ENDIF
    
        * если текущая строка пустая - пропускаем ее
        IF empty(S)
          S = fgets(h,512)
        ENDIF
        
        * отыскиваем текущий элемент в файле с результатами DIR и 
        * преобразуем его нужным способом
        success = .F.
        DO WHILE not feof(h) and not success
          S = fgets(h, 512)
          IF empty(left(S,12)) or empty(substr(S,45))
            LOOP
          ENDIF
        
          short_nm = left(S, 12)
          IF not empty(right(m.short_nm,3))
            short_nm = trim(left(m.short_nm,8)) + '.' + right(m.short_nm,3)
          ELSE
            short_nm = trim(left(m.short_nm,8))
          ENDIF
          short_nm = trim(m.short_nm)
          
          long_nm = trim(substr(S, 45))
          
          IF not m.long2short
            success = (upper(m.cur_nm) == upper(m.long_nm)) or ;
                      (upper(m.cur_nm) == upper(m.short_nm))
          ELSE
            * длинное имя в короткое
            IF empty(m.short_nm)
              success = (upper(m.cur_nm) == upper(m.long_nm))
            ELSE         
              success = (upper(m.cur_nm) == upper(m.short_nm))
            ENDIF
          ENDIF
        ENDDO
      
        = fclose(h)
        ERASE (m.tmp_nm)
      
        IF not m.success
          EXIT
        ENDIF
        
        IF not m.long2short
          result = m.result + m.long_nm
        ELSE
          IF not empty(m.short_nm)
            result = m.result + m.short_nm
          ELSE
            result = m.result + m.long_nm
          ENDIF
        ENDIF
    
        
        IF m.is_dir
          result = m.result + '\'
        ENDIF
        
        IF not m.is_dir
          EXIT
        ENDIF
          
      ENDFOR
    
      IF m.success and not empty(m.result)
        result = m.drv + m.result
      ENDIF
      
    RETURN m.result
    

    Программа работала под Windows 98 и Windows XP, для них способ конвертации имен несколько отличался.

    Точно так же можно было бы организовать переименование файлов с длинными именами, используя команду RENAME, или копирование файлов (команда COPY)


    1. IDDQDesnik
      24.10.2023 17:35

      Создание имен 8.3 сейчас много где выключено по соображения производительности. Проверить можно выполнив от Администратора: fsutil 8dot3name query C: