Команда cp: правильное копирование папок с файлами в *nix +67





В этой статье будут раскрыты некоторые неочевидные вещи связанные с использованием wildcards при копировании, неоднозначное поведение команды cp при копировании, а также способы позволяющие корректно копировать огромное количество файлов без пропусков и вылетов.

Допустим нам нужно скопировать всё из папки /source в папку /target.

Первое, что приходит на ум это:

cp /source/* /target

Сразу исправим эту команду на:

cp -a /source/* /target

Ключ -a добавит копирование всех аттрибутов, прав и добавит рекурсию. Когда не требуется точное воспроизведение прав достаточно ключа -r.

После копирования мы обнаружим, что скопировались не все файлы — были проигнорированы файлы начинающиеся с точки типа:

.profile
.local
.mc

и тому подобные.

Почему же так произошло?

Потому что wildcards обрабатывает shell (bash в типовом случае). По умолчанию bash проигнорирует все файлы начинающиеся с точек, так как трактует их как скрытые. Чтобы избежать такого поведения нам придётся изменить поведение bash с помощью команды:

shopt -s dotglob

Чтобы это изменение поведения сохранилось после перезагрузки, можно сделать файл wildcard.sh c этой командой в папке /etc/profile.d (возможно в вашем дистрибутиве иная папка).

А если в директории-источнике нет файлов, то shell не сможет ничего подставить вместо звёздочки, и также копирование завершится с ошибкой. Против подобной ситуации есть опции failglob и nullglob. Нам потребуется выставить failglob, которая не даст команде выполниться. nullglob не подойдёт, так как она строку с wildcards не нашедшими совпадения преобразует в пустую строку (нулевой длины), что для cp вызовет ошибку.

Однако, если в папке тысячи файлов и больше, то от подхода с использованием wildcards стоит отказаться вовсе. Дело в том, что bash разворачивает wildcards в очень длинную командную строку наподобие:

cp -a /souce/a /source/b /source/c …… /target

На длину командной строки есть ограничение, которое мы можем узнать используя команду:

getconf ARG_MAX

Получим максимальную длину командной строки в байтах:

2097152

Или:

xargs --show-limits

Получим что-то типа:

….
Maximum length of command we could actually use: 2089314
….

Итак, давайте будем обходиться вовсе без wildcards.

Давайте просто напишем

cp -a /source /target

И тут мы столкнёмся с неоднозначностью поведения cp. Если папки /target не существует, то мы получим то, что нам нужно.

Однако, если папка target существует, то файлы будут скопированы в папку /target/source.

Не всегда мы можем удалить заранее папку /target, так как в ней могут быть нужные нам файлы и наша цель, допустим, дополнить файлы в /target файлами из /source.

Если бы папки источника и приёмника назывались одинаково, например, мы копировали бы из /source в /home/source, то можно было бы использовать команду:

cp -a /source /home

И после копирования файлы в /home/source оказались бы дополненными файлами из /source.

Такая вот логическая задачка: мы можем дополнить файлы в директории-приёмнике, если папки называются одинаково, но если они отличаются, то папка-исходник будет помещена внутрь приёмника. Как скопировать файлы из /source в /target с помощью cp без wildcards?

Чтобы обойти это вредное ограничение мы используем неочевидное решение:

cp -a /source/. /target

Те кто хорошо знаком с DOS и Linux уже всё поняли: внутри каждой папки есть 2 невидимые папки "." и "..", являющиеся псевдопапками-ссылками на текущую и вышестоящие директории.

  • При копировании cp проверяет существование и пытается создать /target/.
  • Такая директория существует и это есть /target
  • Файлы из /source скопированы в /target корректно.

Итак, вешаем в жирную рамочку в своей памяти или на стене:

cp -a /source/. /target

Поведение этой команды однозначно. Всё отработает без ошибок вне зависимости от того миллион у вас файлов или их нет вовсе.

Выводы


Если нужно скопировать все файлы из одной папки в другую, не используем wildcards, вместо них лучше использовать cp в сочетании с точкой в конце папки-источника. Это скопирует все файлы, включая скрытые и не завалится при миллионах файлов или полном отсутствии файлов.

Послесловие


vmspike предложил аналогичный по результату вариант команды:

cp -a -T /source /target

Oz_Alex
cp -aT /source /target

ВНИМАНИЕ: регистр буквы T имеет значение. Если перепутать, то получите полную белиберду: направление копирования поменяется.

Благодарности:

  • Компании RUVDS.COM за поддержку и возможность публикации в своем блоге на Хабре.
  • За изображение TripletConcept. Картинка очень большая и детальная, можно открыть в отдельном окне.

P.S. Замеченные ошибки направляйте в личку. Повышаю за это карму.



Вы можете помочь и перевести немного средств на развитие сайта

Теги:



Комментарии (59):

  1. svk28
    /#20751894 / +1

    И тут мы столкнёмся с неоднозначностью поведения cp. Если папки /target не существует, то мы получим то, что нам нужно.

    Однако, если папка target существует, то файлы будут скопированы в папку /source/target.

    А не /target/source?

    • inetstar
      /#20751898

      Исправлено. В след. раз в личку, пожалуйста.

      • karavan_750
        /#20755004

        Ваше требование оправдано, если речь об орфографии.
        Но технические ошибки, влияющие на результат описываемой процедуры или на понимание процесса, должны корректироваться публично.
        Это логично для случая, когда вы пропустите поправку в личке, у читающего статью останется шанс дополнить картину чтением комментов.

        • inetstar
          /#20755060

          Тут была очевидная опечатка.

  2. vmspike
    /#20751946 / +1

    Мне кажется вместо этого лучше использовать опцию -T:
    cp -a -T /source /target


    -T, --no-target-directory
              treat DEST as a normal file

    В некоторых случаях ещё полезен флаг -t, только тогда source и target меняются местами:


    SYNOPSIS
           cp [OPTION]... [-T] SOURCE DEST
           cp [OPTION]... SOURCE... DIRECTORY
           cp [OPTION]... -t DIRECTORY SOURCE...

    • inetstar
      /#20751994

      А можете объяснить, чем именно лучше?

      • vmspike
        /#20752026

        Это меньше похоже на хак, более явно указывается на то, что DEST это именно имя в которое нужно копировать. Плюс у некоторых файловых систем может не быть директорий .. и .

        • inetstar
          /#20752058

          А у каких файловых систем нет директорий ".." и "."?

          • vmspike
            /#20752262

            Когда-то натыкался на инфу, что некоторые файловые системы, могут не иметь этих хардлинков (помню в пример приводились фс для оптических дисков типа UDF) и, вроде, были опции для монтирования с их эмуляцией.
            Вполне возможно, что это уже пережиток прошлого и такого теперь не бывает.

            • khim
              /#20752788 / +1

              Вполне возможно, что это уже пережиток прошлого и такого теперь не бывает.
              Не бывает. В этом легко убедиться.

              $ dd if=/dev/zero of=tmpfile bs=1M count=1
              1+0 records in
              1+0 records out
              1048576 bytes (1.0 MB, 1.0 MiB) copied, 0.00239023 s, 439 MB/s
              $ mkfs.fat tmpfile 
              mkfs.fat 4.1 (2017-01-24)
              $ mmd -i tmpfile test
              $ mdir -i tmpfile test
               Volume in drive : has no label
               Volume Serial Number is D0A1-1DD1
              Directory for ::/test
              
              .            <DIR>     2019-10-14  14:41 
              ..           <DIR>     2019-10-14  14:41 
                      2 files                   0 bytes
                                        1 026 048 bytes free
              
              $ mkdir tmpdir
              $ sudo mount -o loop tmpfile tmpdir
              $ ls -al tmpdir/test/
              total 18
              drwxr-xr-x 2 root root  2048 Oct 14  2019 .
              drwxr-xr-x 3 root root 16384 Jan  1  1970 ..
              


              Как легко заметить информация про .. — разная для mdir и ls. Почему? Потому что ядро игнорирует . и .., которые могут существовать (а могут и не существовать) на диске. Вместо этого . и .. эмулируются внутри ядра.

              Так что в Linux вы никогда не увидите файловых систем без .. В Windows — да, возможно.

              • mayorovp
                /#20755638

                В каком смысле "в Windows — да, возможно"? Разве . и .. не точно так же эмулируются?

                • khim
                  /#20755826

                  В Windows 9X — возможно на 100%, там всё как в DOS. Сегодня… Я понятия не имею кто и как это делает в Windows 10 и не может ли какой-нибудь драйвер IFS сделать так, чтобы. и… в каталоге просто не было. Вот реально — не знаю.

                  В Linux это делается на уровне VFS (и всегда делалось на уровне VFS) и до драйвера дело просто не доходит…

                  • mayorovp
                    /#20755938

                    Мне не удалось найти точной информации содержится ли запись .. в директории NTFS, но думаю что вряд ли — это слишком расточительно.


                    В NTFS, в отличии от других систем, первичным хранилищем информации о файлах являются не записи в директории, а записи в MFT. Содержимое директорий же — лишь B-tree индекс, как в базах данных. И у каждого файла есть по атрибуту $FILE_NAME на каждую директорию, в которой тот находится.


                    Если бы в директориях были записи .. — это бы означало, что у каждой директории есть столько атрибутов $FILE_NAME, сколько у неё субдиректорий. А поскольку все атрибуты хранятся в плоском массиве — это бы убило всю идею B-tree индексов.


                    Так что, если только NTFS делали не полные идиоты, физически .. как запись директории там точно не хранится.


                    А вот через API эта запись ещё как возвращается, так что...

                    • khim
                      /#20756550

                      А вот через API эта запись ещё как возвращается, так что...
                      Совешенно не «так что». Кроме FAT и NTFS есть ведь всякие ISO 9660, UFS и прочие всякие BTRFS. И вот вопрос: всегда ли они эмулируют . и .. — или это от драйвера зависит?

                      По логике-то должно быть как в Linux: . и .. эмулируются VFS, частью ядра, до драйвера дело не доходит в принципе… но я видел много мест в Windows, где есть подобные layering violations, так что ответить на этот вопрос не могу.

    • Oz_Alex
      /#20752198

      Почему не -aT? Минус пробел, минус "-".

      • vmspike
        /#20752232

        Для cp так действительно короче, но бывают программы где склеивание опций приводит к склеиванию ласт неожиданным результатам.

        • Dolios
          /#20754076

          Так вроде стандарт же, однобуквенные опции перечисляются после одного минуса без пробелов, а перед многобуквенными опциями ставят 2 минуса.

          • vmspike
            /#20754318

            Это скорее не стандарт, а обычай, и далеко не все ему следуют (взять хотя бы firefox с его многобуквенными опциями с одним дефисом). И даже по этому стандартному обычаю есть опции, которым требуется аргумент, и не дай божа случайно засунуть другую однобуквенную опцию между многобуквенной и её аргументом (чтобы уточнить поведение, например: cp -aTi /source /target).
            Ну и некоторые программы могут вообще не распознать склеенные аргументы, даже если они однобуквенные безаргументные, ибо разработчику интересно прогать, а не аргументы ваши клееные парсить.

      • inetstar
        /#20752248

        Такой вариант тоже добавлен в статью. Это на любителя. Перепутаете регистр T и будет полная ерунда: поменяется направление копирования.

        • SlavniyTeo
          /#20757102 / +1

          А перепутаете ./source/. и ./source/.. и тоже белиберда получится.


          Или ./source /..


          Вообще, не надо в командной строке путать что-то.

  3. WebMonet
    /#20752072

    объясните, пожалуйста, разницу между
    > cp /a /b
    > cp /a/ /b/
    > cp /a/* /b

    Как правильно скопировать все значимое содержимое одной папки в другую, при этом находясь в третьей?

    • inetstar
      /#20752224

      Тут нужно использовать ключи -a или хотя бы -r для рекурсии.
      Между первой и второй строкой разницы нет.

      А вот в третьей мы приплетаем shell, и если в папке нет файлов или есть начинающиеся с точки, то копирование будет произведено не полностью или с ошибкой.

      • DerRotBaron
        /#20755954

        Между первой и второй строкой разницы нет.

        В BSD Coreutils (как минимум в FreeBSD и OS X) это работает несколько не так: / после первого аргумента копирует не директорию, а ее содержимое


        Пример
        $ mkdir -p a/b/c d
        $ cp -r a/ d && ls d   
        b
        $ cp -r a d && ls d
        a       b

      • SlavniyTeo
        /#20757132

        Первый и второй варианты отличаются в случае, если a — символьная ссылка.


        Тогда первая команда скопирует ссылку, а вторая — будет копировать файлы.

        • inetstar
          /#20763470

          Интересный факт, что добавление слэша инициирует переход по ссылке в каталог.

  4. DaemonGloom
    /#20752184

    Эх. Я думал вы тут расскажете про тонкости копирования с хард-линками, софт-линками и ситуациями, когда файлы находятся на разных ФС.
    А для вашего вопроса есть простой ответ. Нужно использовать cp так:
    rsync -a source_dir target_dir
    Попутно можно использовать параметр --progress, если хочется видеть проценты выполнения задачи.

    • inetstar
      /#20752208

      Есть десятки способов скопировать файлы под Linux.
      В этой статье речь именно о cp.

      Вообще-то я хочу в следующих статьях затронуть и ваши вопросы.
      Просто это слишком много для одной статьи.

    • engine9
      /#20753144 / -1

      А как запускать dd чтобы был виден прогресс? (оффтоп)

      • Prototik
        /#20753792 / +2

        dd status=progress if=... of=...

        • Lirein
          /#20755168

          А если так получилось что забыли, то в параллельной консоли или Ctrl+Z, bg и

          killall -USR1 dd

    • Self_Perfection
      /#20754266 / +2

      Вот только вы слэш пропустили и всё будет скопировано в target_dir/source_dir

      А надо так:

      rsync -a source_dir/ target_dir

      И то есть риск потерять разные атрибуты или хардлинки. Чтоб совсем ничего не потерять
      rsync -aHAX

      • Meklon
        /#20755344

        У меня обычно набор выглядит как rsync -axv

  5. Andrey_Rogovsky
    /#20752916

    Правильный cp — это rsync

    • engine9
      /#20753258 / -1

      Раз уж его упомянули, будьте добры, поясните про завершающие слэши. Это «фишка» самого rsync-a или общая логика работы с путями в GNU/linux? И чем отличается путь с указанным в конце слешем от пути без него? Не могу осилить пояснение на английском.

      • Prototik
        /#20753810 / +1

        Это поведение самого rsync'a, который прямо ему говорит — копируй содержимое директории, а не включай тут вангу с определением "а существует ли в точке назначения такая-то директория, если да то тудааа, если нет то создаёёёём"...


        Эдакий source/., упомянутый в начале статьи, только чуть удобнее.

    • funca
      /#20754754

      Вместо cp можно использовать tar. Как-то так
      tar cf — . | tar xvf — -C /dest

  6. vodopad
    /#20753022

    Поэтому я всегда использую rsync c ключиком -a. Ещё добавляю --progress, чтобы смотреть прогресс.

    • Tangeman
      /#20753980

      Тогда уж не забывайте про -HAX, а то конфуз может случится.

      • isden
        /#20755010

        Тут можно еще напомнить, что в некоторых версиях rsync например нет -AX.

        • Tangeman
          /#20759084

          Поддержка -AX появилась в версии 3.0, а это было аж в 2008 году. Шанс встретить версию ниже очень мал.

          • isden
            /#20759196

            $> rsync --help | grep archive
            -a, --archive archive mode; same as -rlptgoD (no -H)
            $> rsync --version
            rsync version 2.6.9 protocol version 29

    • khim
      /#20754278

      А rsync умеет в --reflink=always? Прогресс — это хорошо, конечно, но зачем же место на диске тратить…

  7. alexxz
    /#20754632

    Рекомендую вообще нафиг отказаться от использования вайлдкардов для подстановок в автоматических скриптах. В простейшем случае в имени может оказаться просто пробел и это порвет аргументы и приведет к неожиданному поведению. В более сложном примере имя файла может быть коротким и начинаться с минуса и команда интерпретирует имя как параметры выполнения.

    • alexxz
      /#20754682 / +2

      Вот простой пример, когда имя файла интерпретируется как опция.
      $ mkdir test
      $ cd test
      $ echo 123 > --help
      $ cp * 124
      Usage: cp [OPTION]… [-T] SOURCE DEST

    • Tangeman
      /#20754982

      Осторожность конечно не помешает, но пробел и прочие мета-символы ничего не порвут (в современных шеллах, по крайней мере), единственная реальная проблема это минус в начале, но и это решается добавлением "--" перед вайлдкардом.

      • khim
        /#20755840 / +1

        Добавить ./ перед вайдлкардом — надёжнее, чем --

      • alexxz
        /#20757232

        Интересно, полез читать что, как и когда. Вобщем опцию complete_fullquote включили по умолчанию в bash4.2 который зарелизился в 2011 году. www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html
        Потыкал в разные дистрибутивы через докер и реально везде работает.
        Вот пример как тыкал

        sudo docker run ubuntu:18.04 bash -l -c 'touch "myfile1 1"; stat myfile*'

        Спасибо

        • khim
          /#20758318

          bash 4.2 мог зарелизится когда угодно, но в MacOS (даже в macOS Catalina, вышедшей неделю назад) используется bash 3.2.57(1), о чём не стоит забывать…

          • alexxz
            /#20763062

            В комментарии ниже показали, что я ошибся в датировке, когда это заработало. Вероятно, гораздо раньше.

        • Tangeman
          /#20758902

          complete_fullquote это вообще не про глоббинг, это только для автодополнения.

          • alexxz
            /#20763042

            Хмм, и правда. Спасибо за дополнение. Тогда я понятия не имею, когда это начало работать. Вероятно, очень давно. А, возможно, у меня в воспоминаниях остались только случаи небезопасного использования переменных типа таких.

            $ touch 'a a'
            
            $ for file in *; do stat $file ; done
            stat: cannot stat 'a': No such file or directory
            stat: cannot stat 'a': No such file or directory
            
            $ for file in *; do stat "$file" ; done
              File: a a
              Size: 0         	Blocks: 16         IO Block: 4096   regular empty file
            Device: 35h/53d	Inode: 13125328    Links: 1
            

      • saege5b
        /#20766792

        Весной 2019 исплевался, экранируя многочисленные пробелы и скрытые символы в именах файлов.
        И дело не только в шелле, а в том, что куча софта по разному видит проблему имени файла.
        Кубунту и Манджаро.

  8. keydon2
    /#20755678

    <зануда>
    В файловой системе директории, а не папки.
    <\зануда>

  9. Andrey123321
    /#20763352

    cp -dpRx /source /target

    • inetstar
      /#20763406

      Насколько я понимаю ваша команда почти полностью эквивалентна:

      cp -ax /source /target


      За исключение того, что ваша версия не сохраняет расширенные аттрибуты типа: context, links, xattr, all. А также неоднозначно ведёт себя в зависимости от существования /target.

      Для однозначности нужно:

      cp -ax /source/. /target

      • Andrey123321
        /#20763576

        Для большей однозначности можно добавить -T

        cp -dpRxT /source /target

        Оставаться в пределах одной файловой системы позволяет -x. Мягкие и жёсткие линки копирует. Насчёт расширенных аттрибутов context, links, xattr, all — ими не пользовался.