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

  • Tutorial


В этой статье будут раскрыты некоторые неочевидные вещи связанные с использованием 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. Замеченные ошибки направляйте в личку. Повышаю за это карму.



RUVDS.com
1 514,36
RUVDS – хостинг VDS/VPS серверов
Поддержать автора
Поделиться публикацией

Комментарии 62

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

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

    А не /target/source?
      0
      Исправлено. В след. раз в личку, пожалуйста.
        +2
        Ваше требование оправдано, если речь об орфографии.
        Но технические ошибки, влияющие на результат описываемой процедуры или на понимание процесса, должны корректироваться публично.
        Это логично для случая, когда вы пропустите поправку в личке, у читающего статью останется шанс дополнить картину чтением комментов.
          +3
          Тут была очевидная опечатка.
      +3

      Мне кажется вместо этого лучше использовать опцию -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...
        0
        А можете объяснить, чем именно лучше?
          +1

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

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

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

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

                $ 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 — да, возможно.
                  0

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

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

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

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


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


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


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


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

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

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

                      0
                      А если в /dev/kmem покопаться — так ещё и не такое можно увидеть.

                      И даже в вашем случае вы отлично можете сделать что-нибудь типа mkdir ./figvam — и файлик отлично создастся. Более того, даже если вы удалите .. — вы всё равно сможете сделать ls ..

                      Но да, с коррапнутыми файловыми системами возможны чудеса… вплодо до kernel oops… Неопределённое поведение — оно такое.
            +1
            Почему не -aT? Минус пробел, минус "-".
              0

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

                0

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

                  +1

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

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

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


                Или ./source /..


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

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

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

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

                В 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
                  0
                  Спасибо за интересный факт!

                  А в Linux (GNU coreutils) именно так, как сказал я:

                  mkdir -p a/b/c d
                  cp -r a/ d && ls d
                  a
                  cp -r a d && ls d
                  a


                  Установка coreutils на Мак.
                  +2

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


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

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

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

                      +2

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

                        0
                        А если так получилось что забыли, то в параллельной консоли или Ctrl+Z, bg и
                        killall -USR1 dd
                      +2
                      Вот только вы слэш пропустили и всё будет скопировано в target_dir/source_dir

                      А надо так:
                      rsync -a source_dir/ target_dir

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

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

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

                          +1

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


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

                            0
                            Спасибо!
                          0

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

                          0
                          Поэтому я всегда использую rsync c ключиком -a. Ещё добавляю --progress, чтобы смотреть прогресс.
                            0
                            Тогда уж не забывайте про -HAX, а то конфуз может случится.
                              0
                              Тут можно еще напомнить, что в некоторых версиях rsync например нет -AX.
                                0
                                Поддержка -AX появилась в версии 3.0, а это было аж в 2008 году. Шанс встретить версию ниже очень мал.
                                  0
                                  $> rsync --help | grep archive
                                  -a, --archive archive mode; same as -rlptgoD (no -H)
                                  $> rsync --version
                                  rsync version 2.6.9 protocol version 29
                              0
                              А rsync умеет в --reflink=always? Прогресс — это хорошо, конечно, но зачем же место на диске тратить…
                              +1

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

                                +4

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

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

                                    +1
                                    Добавить ./ перед вайдлкардом — надёжнее, чем --
                                      0
                                      Интересно, полез читать что, как и когда. Вобщем опцию 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*'

                                      Спасибо
                                        0
                                        bash 4.2 мог зарелизится когда угодно, но в MacOS (даже в macOS Catalina, вышедшей неделю назад) используется bash 3.2.57(1), о чём не стоит забывать…
                                          0
                                          В комментарии ниже показали, что я ошибся в датировке, когда это заработало. Вероятно, гораздо раньше.
                                          +1
                                          complete_fullquote это вообще не про глоббинг, это только для автодополнения.
                                            0
                                            Хмм, и правда. Спасибо за дополнение. Тогда я понятия не имею, когда это начало работать. Вероятно, очень давно. А, возможно, у меня в воспоминаниях остались только случаи небезопасного использования переменных типа таких.
                                            $ 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
                                            
                                          +1
                                          Весной 2019 исплевался, экранируя многочисленные пробелы и скрытые символы в именах файлов.
                                          И дело не только в шелле, а в том, что куча софта по разному видит проблему имени файла.
                                          Кубунту и Манджаро.
                                        –1

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

                                          +4
                                          Ну какой же вы зануда, если применяете \ там, где должен быть /
                                          0
                                          cp -dpRx /source /target
                                            0
                                            Насколько я понимаю ваша команда почти полностью эквивалентна:

                                            cp -ax /source /target


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

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

                                            cp -ax /source/. /target
                                              0
                                              Для большей однозначности можно добавить -T
                                              cp -dpRxT /source /target

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

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                          Самое читаемое