В этой статье будут раскрыты некоторые неочевидные вещи связанные с использованием 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
    14.10.2019 14:01
    +1

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

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

    А не /target/source?


    1. inetstar Автор
      14.10.2019 14:02

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


      1. karavan_750
        15.10.2019 01:12

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


        1. inetstar Автор
          15.10.2019 01:57

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


  1. vmspike
    14.10.2019 14:07
    +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...


    1. inetstar Автор
      14.10.2019 14:11

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


      1. vmspike
        14.10.2019 14:15

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


        1. inetstar Автор
          14.10.2019 14:18

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


          1. vmspike
            14.10.2019 14:48

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


            1. khim
              14.10.2019 16:10
              +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 — да, возможно.


              1. mayorovp
                15.10.2019 09:11

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


                1. khim
                  15.10.2019 10:01

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

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


                  1. mayorovp
                    15.10.2019 10:22

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


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


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


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


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


                    1. khim
                      15.10.2019 12:03

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

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


    1. Oz_Alex
      14.10.2019 14:37

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


      1. vmspike
        14.10.2019 14:41

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


        1. Dolios
          14.10.2019 20:16

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


          1. vmspike
            14.10.2019 21:15

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


            1. funca
              14.10.2019 22:46
              +1

              Это ваш странный обычай называется POSIX. https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html, частью которого является команда cp
              https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cp.html .


      1. inetstar Автор
        14.10.2019 14:44

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


        1. SlavniyTeo
          15.10.2019 13:15
          +1

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


          Или ./source /..


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


  1. WebMonet
    14.10.2019 14:20

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

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


    1. inetstar Автор
      14.10.2019 14:40

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

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


      1. DerRotBaron
        15.10.2019 10:25

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

        В 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


        1. inetstar Автор
          15.10.2019 13:34

          Спасибо за интересный факт!

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

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


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


      1. SlavniyTeo
        15.10.2019 13:18

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


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


        1. inetstar Автор
          16.10.2019 13:22

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


  1. DaemonGloom
    14.10.2019 14:35

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


    1. inetstar Автор
      14.10.2019 14:38

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

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


    1. engine9
      14.10.2019 17:16
      -1

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


      1. Prototik
        14.10.2019 19:02
        +2

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


        1. Lirein
          15.10.2019 05:09

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

          killall -USR1 dd


    1. Self_Perfection
      14.10.2019 21:02
      +2

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

      А надо так:

      rsync -a source_dir/ target_dir

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


      1. Meklon
        15.10.2019 07:25

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


  1. Andrey_Rogovsky
    14.10.2019 16:38

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


    1. engine9
      14.10.2019 17:32
      -1

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


      1. Prototik
        14.10.2019 19:05
        +1

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


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


        1. engine9
          14.10.2019 21:57

          Спасибо!


    1. funca
      14.10.2019 23:13

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


  1. vodopad
    14.10.2019 16:56

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


    1. Tangeman
      14.10.2019 19:46

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


      1. isden
        15.10.2019 01:14

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


        1. Tangeman
          15.10.2019 18:44

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


          1. isden
            15.10.2019 19:00

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


    1. khim
      14.10.2019 21:04

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


  1. alexxz
    14.10.2019 22:40

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


    1. alexxz
      14.10.2019 22:51
      +2

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


    1. Tangeman
      15.10.2019 00:50

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


      1. khim
        15.10.2019 10:03
        +1

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


      1. alexxz
        15.10.2019 13:30

        Интересно, полез читать что, как и когда. Вобщем опцию 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*'

        Спасибо


        1. khim
          15.10.2019 16:21

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


          1. alexxz
            16.10.2019 12:17

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


        1. Tangeman
          15.10.2019 18:08

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


          1. alexxz
            16.10.2019 12:15

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

            $ 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. saege5b
        17.10.2019 00:34

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


  1. keydon2
    15.10.2019 09:26

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


  1. Andrey123321
    16.10.2019 13:05

    cp -dpRx /source /target


    1. inetstar Автор
      16.10.2019 13:10

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

      cp -ax /source /target


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

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

      cp -ax /source/. /target


      1. Andrey123321
        16.10.2019 13:37

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

        cp -dpRxT /source /target

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