Как правильно скопировать файлы и папки исключая некоторые из них

Топик написан в ответ на похожий.

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

Но главная проблема этого подхода в другом — он не соответствует идеологии unix: сложные задачи решаются комбинацией простых утилит.

Под катом подробности о методах решения этого класса задач — не рассматривайте это как готовый рецепт.


0. Декомпозиция


Решение любой комплексной задачи начинается с разбора её на составные части. Итак нам нужно скопировать некоторый набор файлов предварительно его отфильтровав.
Значит — получение списка файлов, фильтрация, копирование.

1. Получение списка файлов



Обычно мы просматриваем список файлов программой ls. Её вывод выглядит примерно так:
$ ls -1
dir1
dir2
file1.bin
file2.txt

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

Следующая программа, которая приходит на ум — find
$ find ./
./
./dir1
./dir1/file7.txt
./dir2
./file1.bin
./file2.txt

Уже лучше но в вывод попали и директории, а они нам не нужны. Попробуем так:
$ find ./ -type 'f'
./dir1/file7.txt
./file1.bin
./file2.txt


Вот то, что там нужно. Список файлов.

2. Фильтрация



Этот список файлов нужно отфильтровать. Перенаправим вывод нашей предыдущей комманды в программу grep.
$ find ./ -type 'f' | grep 2
./dir2
./file2.txt


Хорошо, но в условиях задачи стоит исключать файлы, так что немного поменеяем наш конвейер
$ find ./ -type 'f' | grep -v 2
./dir1/file7.txt
./file1.bin


Первые две части выполнены.

3. Копирование



Из man-страницы для команды cp мы можем узнать, что исходный файл нужно передавать программе cp в качестве аргумента, а мы пока можем только перенаправить список на стандартный ввод.
Применим утилиту xargs — она принимает стандартный ввод и вызывает указанную программу с параметрами из стандартного ввода. Итак:
$  find ./ -type 'f' | grep -v 2 | xargs -n 1 -I % cp --parents  "%"  /path/to/dest/dir/

-n 1 значит, что только одна строка из стандартного ввода подставляется в комманду, а -I % — определяет символ, который будет заменен в целевой комманде на строчку из стандартного ввода. В нашем случае это будет
 cp --parents  "./dir1/file7.txt"  /path/to/dest/dir/
 cp --parents  "./file1.bin"  /path/to/dest/dir/


Можно считать, что задача решена.

Вместо заключения


Я надеюсь что это описание поможет правильно подходить к решению как таких простых так и более комплексных задач.

Хочется отметить, что
  • Это топик способах решения задач и немного о применении конвейера, а не о копировании файлов
  • Этот способ далеко не едиственный и даже не самый короткий, а наиболее наглядный для демонстрации методологии решения.
  • В случае этой конкретной задачи будет быстрее воспользоваться find ./ -type f ! -name "*2*" -exec cp --parents -t /target/dir "{}" \+
  • Лично я воспользовался-бы tar --exclude=2 -cf - ./ | ( cd /path/to/dest/ && tar -xvf - )
  • Т.к. это первый мой топик, буду рад конструктивной критике

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

    +6
    Написали же ответ в комментариях, с нормальным решением в одну команду! man rsync.
      +11
      Мало того, rsync — не единственный способ. Однако, во-первых, он не в входит в стандартную поставку множества дистрибутивов. А во вторых — цель топика не показать одну строчку «как сделать», а рассказать, о том как самому искать решения не изобретая велосипедов.

      Или вы думаете что топику не место на хабре?
        +2
        этому топику — место. исходному — место в QA
          –1
          этому топику — место
          ну скажем так, вроде информация годная, но вот статьёй это назвать тяжело. Это можно свернуть в комментарий к предыдущей «статье», до фразы: «смотри на find и прочитай в man'е, что у cp есть ключ --parents».
            +1
            а у find есть ключи name и exec. а так же возможность комбинировать ключи через опции -o, -a и скобки. Но кому это интересно?) Читать маны — не модно.
              +3
              «читаю man'ы — $10, читаю man'ы с выражением — $15» ©
                0
                exec, если я не ошибаюсь, есть только в GNU find
                -name и ещё -regex, если уж говорить об исходной задаче — укоротили-бы решение, но главной целью было показать возможности конвейера.
                  +1
                  Конвейер не предназначен для передачи имён файлов. В ссылке ниже подробное объяснение этому чуду.
                  exec есть и в POSIX версии. Даже + на конце exec'a там есть, если я не ошибаюсь.
                    0
                    Да, вы правы про exec, видимо у меня сложилось неверное впечатление после того как лет 10 назад я не нашел его в man-e
                      0
                      10 лет прошло — и вот решение найдено :)
                      +2
                      С каких пор конвейер не предназначен для передачи имен файлов? Кажется, у Вас съелась ссылка, которую Вы хотели показать.

                      Статья, на мой взгляд, хорошая, на простом примере показывает начинающим, как решить некоторую задачу, объединив несколько простых утилит.
                        +1
                        Ссылка не съелась. Я просто ссылался на свой комментарий ниже.
                          +1
                          Кстати, хорошая статья, спасибо. Но это уже высокое кун-фу, я думаю, что начинающих не стоит сразу запутывать, достаточно показать флаг --print0 / -0 и упомянуть, что в случае «необычных» файлов стоит насторожиться.

                          Подобные «особенности» именования не отменяют удобства применения ls, find, xargs, for i in `find..` и других в 99% практических ситуациях.

                          Спецсимволов бояться — xargs не запускать.
                  0
                  $ man cp | grep parents | wc -l
                  0
                    0
                    Вы правы, это умеет только GNU cp. В комментариях предложили cpio как замену.
                0
                Я бы сказал, что это не статья, а часть серии статей о том как писать скрипты. И видя заголовок «как решить то-то», я жду решение, а не экскурс в анализ и знание различных утилит окружения GNU.
                  +13
                  Вы знаете, когда я только начинал изучать unix-like системы, главное чему я был поражен — это следование принципу «от понимания к действию» и прочитав одну статью становилось возможным решать весь класс подобных проблем. И этим статьи отличались от «howto» которые как вы и предлагаете — описывали последовательность действий необходимую для достижения конкретного результата в конкретных условиях.

                  Однако, мне кажется, что хабр уж точно не место для однострочных howto. А эта информация, возможно, поможет кому нибудь понять как решаются такие задачи.
                    0
                    Возможны Вы правы, я обычно изучаю что-либо новое для меня методом индукции. В любом, случае дискуссию на тему «howto vs article» я разводить не хочу.
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    Боюсь ошибиться, но я много где его не встречал. В ubuntu 10.04 из не-linux — во freebsd.

                    P.S да, я понимаю, что это решается apt-get install rsync
                    • НЛО прилетело и опубликовало эту надпись здесь
                        0
                        Если и правда интересно, вечером проведу мини-исследование.
                          0
                          В debian, ubuntu в стандартной поставке он точно отсутствует. Скорее всего в большинстве производных тоже.
                          0
                          пишу из под 10.04, rsync есть.
                        0
                        Отличная статья, познавательная. Спасибо!

                        На самом деле многому учит.
                      0
                      Для подстановки аргументов ещё можно использовать конструкцию $() внутрь которой мы помещаем файнд с грепом, а всё это целиком отдаём cp в качестве аргумента.
                        0
                        Да, это возможно, но только в bash, а csh, например, это не умеет.
                          –1
                          это умеет стандартный шелл
                            +1
                            стандартный для какого дистрибутива? у всех могут быть разные «стандартные» шеллы. Даже /bin/sh почти нигде не является тем самым Bourne Shell, а чаще всего ссылкой на /bin/bash или еще куда-то
                        • НЛО прилетело и опубликовало эту надпись здесь
                            0
                            Полезная инфа, буду знать, спасибо :)
                          +6
                          Читать до полного просветления.
                          Автору исходного поста, кстати, тоже не помешало бы.
                          А ещё не помешало бы почитать man find и использовать вместо find | grep -v кошерную конструкцию
                          find /path ! -name "*exclude*"
                          

                          И вообще, если вы такой противник rsync'a, то задача решается одним find'ом на раз два:
                          find /path -type f ! -name "*exclude*" -exec cp --parents -t /target/dir "{}" \+
                          


                          А в целом, в статье я увидел лишь полное нежелание читать маны, стремление использовать странные решения и преподносить это как способ «как самому искать решения не изобретая велосипедов».
                            +2
                            Вы правы, в случае, если в качестве исходного списка файлов у нас выступает файловая система — можно использовать find и множество других утилит. Топиком я хотел показать как решать подобный класс задач — комбинированием утилит. Ведь как только мы начинаем брать список файлов, например из файла, find нам уже не поможет.

                            Вообщем, судя по обилию комментариев с непониманием исходной цели топика, что-то было написано не так и его лучше убрать.
                              –1
                              В таком случае, стоило сделать акцент на этом в самом топике. В противном случае тонны конструктивной критики выльются на вас. И комментирующие будут правы.
                              А в случае чтения списка из файла, стоит тогда включить проверку, есть ли вообще такой файл, и не сломает ли что запускаемая команда.

                              Вы просто выбрали неудачный пример для того, что хотите продемонстрировать.
                                0
                                Внес изменения в исходный топик, надеюсь этого будет достаточно.
                                Спасибо за конструктивность.
                                –1
                                Если вы хотите что-то продемонстрировать, как то «брать список файлов, например из файла» — так и делайте именно так. Не надо подавать дурных примеров. Те, кто будет это читать, скорее всего не знают о том, что умеет find сам по себе, а стоило бы.
                                  +1
                                  Внес изменения в исходный топик, добавлен дискламер, и готовый рецепт для этой конкретной задачи.

                                  Однако не могли-бы Вы пояснить почему вы считаете использование более универсальных методов дурным примером?
                                    +2
                                    Вы же хотите научить хорошему? Тогда надо учить использовать для каждой задачи подходящий инструмент. Я вполне допускаю, что когда времени нет, лень посмотреть ман, и набрал в консоли первое, что в голову пришло — это одно, но не надо это показывать в качестве обучающего материала. Автор, который осмелился учить чему-то остальных, должен нести большую ответственность.

                                    off
                                    Может быть я брюзжу тут как старый дед =), но уж больно универсалов много развелось, орудующих одним молотком по всем гвоздям, специалистов вот только не найти. И куча how-to в сети только способствует.
                                      0
                                      Насколько я понял, проблема в том, что очень общую тему я объяснил на примере излишне конкретной задачи.
                                      Думаю, стоит написать отдельный топик, с большим количеством теории и рассмотрев больше примеров, про который нельзя будет сказать что «это решается одной командой». Но так уж получилось, что я собрался написать статью именно в ответ на тот топик.

                                      off reply
                                      Мне кажется, что непрофессионализм, частным случаем которого является поверхностный подход (естественно, подразумевая ситуации, где такой подход не допустим) — является скорее свойством личности, а не результатом неправильного обучения
                                +1
                                Никогда не понимал, что означает конструкция
                                "{}" \+
                                
                                в конце find-а?
                                  +6
                                  Очень полезная штука. find с \; запускает по одному процессу на каждый найденный файл. find с \+ на конце группирует файлы и запускает по одному процессу на много файлов. Экономия времени и ресурсов, однако. Появилась лет 7 назад.

                                  С вас 10$ за краткую выжимку из man'а =)
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                      +7
                                      Но ведь за каждое использование пайпа там, где без него можно обойтись, бог будет убивать котенка.
                                      Экий вы бездушный
                                        +3
                                        Вы пропустили Большую Амнистию когда на десктопы массово прибыли многоядерные процессора, и переключение контекста при работе с pipe стало куда меньше.
                                          +1
                                          Но в священном писании говорится про каждого котенка за каждый проход пайпа. Не несите ересь.
                                    +1
                                    man find говорит нам, что:
                                    Строка `{}' будет заменена именем текущего обрабатываемого файла

                                      +1
                                      У меня этот вариант работает и без слеша. Но безопаснее/привычнее экранировать.

                                      Вообще есть 2 варианта запуска find -exec:
                                      find -exec echo {} ;
                                      find -exec echo {} +
                                      


                                      Попытка скопировать это и выполнить в шелле провалится, т.к. шелл съест «; ». Чтоб символ дошёл до find, его экранируют от шелла слешем.

                                      Ещё можно взять в кавычки с тем же эффектом:
                                      find -exec echo {} ';'
                                      
                                        +1
                                        Спасибо всем за справку.
                                    0
                                    Вместо 'xargs cp --parents' можно использовать 'cpio -pd'
                                      0
                                      Необязательно писать find ./, достаточно find .. Меньше набирать, и выглядит красивее.
                                        +1
                                        Да, это привычка, да и кнопки всё равно рядом находятся.
                                        0
                                        Годная статья. Спасибо.
                                        Как раз недавно задавался подобным вопросом.
                                        Вообще надо будет ознакомиться и начать использовать такие true-юниксойдные штуки, как sed, awk, xargs, find, for, узнать больше о любимом grep. :) Отпугивает то, что выглядит всё это по-шамански сложно.
                                          +2
                                          Пожалуйста.
                                          Не бойтесь, главное преимущество этого подхода то, что каждая отдельная утилита достаточно проста.
                                          Ваш любимый grep выполняет одну функцию — фильтрует. Просто делает он это кучей разных способов, но вам-же не обязательно изучать их все сразу — просто имейте ввиду что grep может отфильтровать всё что угодно, а конкретные параметры всегда можно посмотреть в man в тот момент, когда они понадобятся.
                                          +2
                                          А у меня есть файлы с переводами строк в именах (да, перевод строки — допустимый символ). Как там grep отработает? Подсказываю: неправильно :)
                                            0
                                            Значит вам, очевидно, нужно будет использовать способы отличные от описанного в основной части топка. Например — один из готовых рецептов в заключении.
                                              0
                                              Я к этому и веду, что стоит написать, что всё это хорошо, но неверно. А сейчас написано «Задача решена», как будто это верное решение. А в заключении эквивалентное решение.
                                                0
                                                Вы правы, исправил формулировку.
                                                  0
                                                  На самом деле тема многострочной фильтрации не сказать, что хорошо раскрыта. На последнюю задачу выковыривания данных из постраничного вывода я убил приличное количество времени, а от переводов избавлялся с помощью tr`а, что ну никак нельзя назвать элегантным решением. Был бы рад увидеть обзорную статью на эту тему, например. Начинание то у вас хорошее.
                                                    0
                                                    Если сильно хочется использовать grep, то тогда нужно в качестве разделителя использовать нулевой символ (гарантировано не часть имени файла).
                                                    find . -type f -print0 | grep -z -v 2 | xargs -0 ...
                                                    

                                                    Такой вариант будет правильным. Может, iamwizard будет тоже интересно.
                                                      0
                                                      Как уже писалось выше, имхо, эта информация излишня для статьи ориентированной на новичков.
                                                      Думаю, что этот и другие интересные варианты решения найдут себе место в более цикле статей ориентированном на более подготовленную публику.
                                                      0
                                                      Если найду время написать, то обязательно затрону тему сепараторов и экранирования
                                              0
                                              Декомпозиция требует первым делом выписать все файлы, а спасает от первичного формирования громадной простыни юниксовая потоковость команд, да? Без возможности организовать поток такой подход был бы чреват неэффективным использованием ресурсов, для предварительного формирования того огромного списка…
                                                0
                                                «Получить список файлов» и «Получить список всех файлов» — это разные вещи, а итеративность возможно организовать и в отсутствии пайпа.
                                                0
                                                некогда озаботился проблемой сложного копирования файлов, пробовал xcopy, robocopy итп и пришел к выводу что во многих ситуациях нужно писать скрипт. Тогда сел и в качестве развлечения написал свою консольную программку для копирования. Основной фишкой которой было разделение ключей для копирования на ключи для файлов и пля папок. Так же реализовал возможность задания множества масок для каждого ключа. С тех пор перестал пользоваться вышеуказанными утилитами и по мере нужды добавляю в свою прогу новые возможности. На данный момент уже получился хороший список, и точно могу сказать, то что в этой проге можно сделать одним запуском проги в других, без написания скрипта не получится. Кому интересно тут последняя альфа. Если кому что то надо добавить обращайтесь.
                                                  0
                                                  маленький пример:
                                                  copymik "c:\Папка откуда" "d:\Папка куда" /MF *.txt *.doc *.pdf /MD Doc* Scan /XCF __*.pdf bak*.doc ~*.pdf /XCD Temp Tmp
                                                  скопирует файлы с масками *.txt *.doc *.pdf исключив файлы с масками __*.pdf bak*.doc ~*.pdf из папок с масками Doc* Scan пропуская в них папки с масками Temp Tmp
                                                  где:
                                                  Заголовок спойлера
                                                  [/MF[ МаскаФайла1[ МаскаФайла2[ ....]]]] Маска для копирования файлов (по умолчанию маска * — все)
                                                  [/MD[ МаскаПапки1[ МаскаПапки2[ ....]]]] Маска для копирования папок (по умолчанию маска * — все)
                                                  [/XCD [МаскаПапки1[ МаскаПапки2[ ....]]]] Не копировать папки с указанными масками (по умолчанию маска * — все)
                                                  [/XCF [МаскаФайла1[ МаскаФайла2[ ....]]]] Не копировать файлы с указанными масками (по умолчанию маска * — все)


                                                  если надо перезаписать то добавить /OF
                                                  если в каких то папках ненадо проверять маски файлов то /XDMF или /XDMD
                                                  Заголовок спойлера
                                                  [/XDMD МаскаПапки1[ МаскаПапки2[ ....]]] Не проверять маску папки для подпапок с указанной маской (будет использована маска * — все)
                                                  [/XDMF МаскаПапки1[ МаскаПапки2[ ....]]] Не проверять файловую маску для подпапок с указанной маской (будет использована маска * — все)

                                                  и.т.п
                                                  в этой программке Вас многое, надеюсь приятно, удивит

                                                    0
                                                    Ребята подскажите как мне исправить.
                                                    find /home/vmail/vp/cur/ -type f -exec grep -H "To: office@vp.com" {} \; | xargs -n 1 -I % cp --parents "%" /home/OFFICE/

                                                    У меня в результате находит файл но при копировании подставляет «To: office@vp.com». А как сделать так что бы подставляло только пусть?
                                                      0
                                                      сам нашел ответ. ключ -l в grep выводит тольк имя файла

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

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