В этой статье будут раскрыты некоторые неочевидные вещи связанные с использованием 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)
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...
inetstar Автор
14.10.2019 14:11А можете объяснить, чем именно лучше?
vmspike
14.10.2019 14:15Это меньше похоже на хак, более явно указывается на то, что
DEST
это именно имя в которое нужно копировать. Плюс у некоторых файловых систем может не быть директорий..
и.
inetstar Автор
14.10.2019 14:18А у каких файловых систем нет директорий ".." и "."?
vmspike
14.10.2019 14:48Когда-то натыкался на инфу, что некоторые файловые системы, могут не иметь этих хардлинков (помню в пример приводились фс для оптических дисков типа UDF) и, вроде, были опции для монтирования с их эмуляцией.
Вполне возможно, что это уже пережиток прошлого и такого теперь не бывает.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 — да, возможно.mayorovp
15.10.2019 09:11В каком смысле "в Windows — да, возможно"? Разве
.
и..
не точно так же эмулируются?khim
15.10.2019 10:01В Windows 9X — возможно на 100%, там всё как в DOS. Сегодня… Я понятия не имею кто и как это делает в Windows 10 и не может ли какой-нибудь драйвер IFS сделать так, чтобы. и… в каталоге просто не было. Вот реально — не знаю.
В Linux это делается на уровне VFS (и всегда делалось на уровне VFS) и до драйвера дело просто не доходит…mayorovp
15.10.2019 10:22Мне не удалось найти точной информации содержится ли запись
..
в директории NTFS, но думаю что вряд ли — это слишком расточительно.
В NTFS, в отличии от других систем, первичным хранилищем информации о файлах являются не записи в директории, а записи в MFT. Содержимое директорий же — лишь B-tree индекс, как в базах данных. И у каждого файла есть по атрибуту $FILE_NAME на каждую директорию, в которой тот находится.
Если бы в директориях были записи
..
— это бы означало, что у каждой директории есть столько атрибутов $FILE_NAME, сколько у неё субдиректорий. А поскольку все атрибуты хранятся в плоском массиве — это бы убило всю идею B-tree индексов.
Так что, если только NTFS делали не полные идиоты, физически
..
как запись директории там точно не хранится.
А вот через API эта запись ещё как возвращается, так что...
khim
15.10.2019 12:03А вот через API эта запись ещё как возвращается, так что...
Совешенно не «так что». Кроме FAT и NTFS есть ведь всякие ISO 9660, UFS и прочие всякие BTRFS. И вот вопрос: всегда ли они эмулируют.
и..
— или это от драйвера зависит?
По логике-то должно быть как в Linux:.
и..
эмулируются VFS, частью ядра, до драйвера дело не доходит в принципе… но я видел много мест в Windows, где есть подобные layering violations, так что ответить на этот вопрос не могу.
Oz_Alex
14.10.2019 14:37Почему не -aT? Минус пробел, минус "-".
vmspike
14.10.2019 14:41Для
cp
так действительно короче, но бывают программы где склеивание опций приводит ксклеиванию ластнеожиданным результатам.Dolios
14.10.2019 20:16Так вроде стандарт же, однобуквенные опции перечисляются после одного минуса без пробелов, а перед многобуквенными опциями ставят 2 минуса.
vmspike
14.10.2019 21:15Это скорее не стандарт, а обычай, и далеко не все ему следуют (взять хотя бы firefox с его многобуквенными опциями с одним дефисом). И даже по этому стандартному обычаю есть опции, которым требуется аргумент, и не дай божа случайно засунуть другую однобуквенную опцию между многобуквенной и её аргументом (чтобы уточнить поведение, например:
cp -aTi /source /target
).
Ну и некоторые программы могут вообще не распознать склеенные аргументы, даже если они однобуквенные безаргументные, ибо разработчику интересно прогать, а не аргументы ваши клееные парсить.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 .
inetstar Автор
14.10.2019 14:44Такой вариант тоже добавлен в статью. Это на любителя. Перепутаете регистр T и будет полная ерунда: поменяется направление копирования.
SlavniyTeo
15.10.2019 13:15+1А перепутаете
./source/.
и./source/..
и тоже белиберда получится.
Или
./source /.
.
Вообще, не надо в командной строке путать что-то.
WebMonet
14.10.2019 14:20объясните, пожалуйста, разницу между
> cp /a /b
> cp /a/ /b/
> cp /a/* /b
Как правильно скопировать все значимое содержимое одной папки в другую, при этом находясь в третьей?inetstar Автор
14.10.2019 14:40Тут нужно использовать ключи -a или хотя бы -r для рекурсии.
Между первой и второй строкой разницы нет.
А вот в третьей мы приплетаем shell, и если в папке нет файлов или есть начинающиеся с точки, то копирование будет произведено не полностью или с ошибкой.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
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 на Мак.
SlavniyTeo
15.10.2019 13:18Первый и второй варианты отличаются в случае, если a — символьная ссылка.
Тогда первая команда скопирует ссылку, а вторая — будет копировать файлы.
inetstar Автор
16.10.2019 13:22Интересный факт, что добавление слэша инициирует переход по ссылке в каталог.
DaemonGloom
14.10.2019 14:35Эх. Я думал вы тут расскажете про тонкости копирования с хард-линками, софт-линками и ситуациями, когда файлы находятся на разных ФС.
А для вашего вопроса есть простой ответ. Нужно использовать cp так:
rsync -a source_dir target_dir
Попутно можно использовать параметр --progress, если хочется видеть проценты выполнения задачи.inetstar Автор
14.10.2019 14:38Есть десятки способов скопировать файлы под Linux.
В этой статье речь именно о cp.
Вообще-то я хочу в следующих статьях затронуть и ваши вопросы.
Просто это слишком много для одной статьи.
Self_Perfection
14.10.2019 21:02+2Вот только вы слэш пропустили и всё будет скопировано в target_dir/source_dir
А надо так:rsync -a source_dir/ target_dir
И то есть риск потерять разные атрибуты или хардлинки. Чтоб совсем ничего не потерятьrsync -aHAX
Andrey_Rogovsky
14.10.2019 16:38Правильный cp — это rsync
engine9
14.10.2019 17:32-1Раз уж его упомянули, будьте добры, поясните про завершающие слэши. Это «фишка» самого rsync-a или общая логика работы с путями в GNU/linux? И чем отличается путь с указанным в конце слешем от пути без него? Не могу осилить пояснение на английском.
Prototik
14.10.2019 19:05+1Это поведение самого rsync'a, который прямо ему говорит — копируй содержимое директории, а не включай тут вангу с определением "а существует ли в точке назначения такая-то директория, если да то тудааа, если нет то создаёёёём"...
Эдакий
source/.
, упомянутый в начале статьи, только чуть удобнее.
vodopad
14.10.2019 16:56Поэтому я всегда использую rsync c ключиком -a. Ещё добавляю --progress, чтобы смотреть прогресс.
khim
14.10.2019 21:04А rsync умеет в
--reflink=always
? Прогресс — это хорошо, конечно, но зачем же место на диске тратить…
alexxz
14.10.2019 22:40Рекомендую вообще нафиг отказаться от использования вайлдкардов для подстановок в автоматических скриптах. В простейшем случае в имени может оказаться просто пробел и это порвет аргументы и приведет к неожиданному поведению. В более сложном примере имя файла может быть коротким и начинаться с минуса и команда интерпретирует имя как параметры выполнения.
alexxz
14.10.2019 22:51+2Вот простой пример, когда имя файла интерпретируется как опция.
$ mkdir test
$ cd test
$ echo 123 > --help
$ cp * 124
Usage: cp [OPTION]… [-T] SOURCE DEST
Tangeman
15.10.2019 00:50Осторожность конечно не помешает, но пробел и прочие мета-символы ничего не порвут (в современных шеллах, по крайней мере), единственная реальная проблема это минус в начале, но и это решается добавлением "--" перед вайлдкардом.
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*'
СпасибоTangeman
15.10.2019 18:08complete_fullquote это вообще не про глоббинг, это только для автодополнения.
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
saege5b
17.10.2019 00:34Весной 2019 исплевался, экранируя многочисленные пробелы и скрытые символы в именах файлов.
И дело не только в шелле, а в том, что куча софта по разному видит проблему имени файла.
Кубунту и Манджаро.
Andrey123321
16.10.2019 13:05cp -dpRx /source /target
inetstar Автор
16.10.2019 13:10Насколько я понимаю ваша команда почти полностью эквивалентна:
cp -ax /source /target
За исключение того, что ваша версия не сохраняет расширенные аттрибуты типа: context, links, xattr, all. А также неоднозначно ведёт себя в зависимости от существования /target.
Для однозначности нужно:
cp -ax /source/. /target
Andrey123321
16.10.2019 13:37Для большей однозначности можно добавить -T
cp -dpRxT /source /target
Оставаться в пределах одной файловой системы позволяет -x. Мягкие и жёсткие линки копирует. Насчёт расширенных аттрибутов context, links, xattr, all — ими не пользовался.
svk28
А не /target/source?
inetstar Автор
Исправлено. В след. раз в личку, пожалуйста.
karavan_750
Ваше требование оправдано, если речь об орфографии.
Но технические ошибки, влияющие на результат описываемой процедуры или на понимание процесса, должны корректироваться публично.
Это логично для случая, когда вы пропустите поправку в личке, у читающего статью останется шанс дополнить картину чтением комментов.
inetstar Автор
Тут была очевидная опечатка.