递归复制文件夹,排除某些文件夹。

222

我想编写一个简单的bash脚本,将一个文件夹中包括隐藏文件和文件夹在内的所有内容复制到另一个文件夹中,但我想排除某些特定的文件夹。如何实现这一目标?


2
我想象一下,类似于使用“find . -name *”管道到“grep /v“exclude-pattern””来过滤掉不需要的文件,然后再通过管道到“cp”来进行复制。 - i_am_jorf
2
我曾试图做类似的事情,但无法弄清如何使用管道与cp命令。 - trobrock
2
这个问题可能应该提交到超级用户。你要找的命令是xargs。你也可以像使用管道连接两个tar命令一样做。 - Kyle Butt
3
或许现在已经有点晚了,这个回答可能无法准确解答问题,但我有一个提示:如果你只想排除目录的直接子目录,你可以利用Bash模式匹配,例如 cp -R !(dir1|dir2) path/to/destination。这将复制除 "dir1" 和 "dir2" 之外的所有内容到指定目的地路径。 - Boris D. Teoharov
2
请注意,!(dir1|dir2) 模式需要打开 extglob(使用 shopt -s extglob 打开)。 - Boris D. Teoharov
真的没有使用 cp 命令完成这个操作的方法吗?糟糕透了。 - Alexander Mills
9个回答

375

使用rsync:

rsync -av --exclude='path1/to/exclude' --exclude='path2/to/exclude' source destination

注意使用 sourcesource/ 是不同的。一个带斜杠的意思是将文件夹 source 的内容复制到 destination 中。如果没有斜杠,就表示将文件夹 source 复制到 destination 中。

或者,如果您有很多要排除的目录(或文件),可以使用 --exclude-from=FILE,其中 FILE 是包含要排除的文件或目录名称的文件名。

--exclude 也可以包含通配符,例如 --exclude=*/.svn*


16
我建议添加 "--dry-run" 选项以检查哪些文件将被复制。 - loretoparisi
2
@AmokHuginnsson - 你在使用哪些系统?我知道包括RHEL、CentOS、Debian和Ubuntu在内的所有主流Linux发行版都默认包含了Rsync,而且我相信FreeBSD也有。 - siliconrockstar
2
对于基于RHEL的发行版:yum install rsync,或基于Debian的版本:apt-get install rsync。除非您正在使用自己的硬件从绝对基础构建服务器,否则这不是问题。rsync在我的Amazon EC2盒子上默认安装了,并且在我的ZeroLag和RackSpace盒子上也是如此。 - siliconrockstar
3
相较于cp命令,rsync命令似乎要慢的多?至少我个人是这样认为的。 - Kojo
4
例如,如果要忽略 Git 目录:rsync -av --exclude='.git/' ../old-repo/ . - nycynik
显示剩余10条评论

50

使用tar命令和管道一起使用。

cd /source_directory
tar cf - --exclude=dir_to_exclude . | (cd /destination && tar xvf - )

你甚至可以在 ssh 上使用这种技术。


这种方法不必要地先将目标源文件打成tar包(并在归档中排除特定目录),然后在目标位置解压。不建议使用! - Waldheri
7
@Waldheri,你错了。这是最佳解决方案。它完全符合 OP 的要求,并且可以在大多数类 Unix 操作系统的默认安装上运行。打包和解包是即时进行的,没有文件系统残留(在内存中),因此打包和解包的代价可以忽略不计。 - AmokHuginnsson
@WouterDonders Tar是最小的开销。它不应用压缩。 - Kyle Butt
4
当你的容器中没有rsync可用且你不想费心安装它时,这很完美。 - Dániel Kis-Nagy
rsync went unavailable for us. So i updated the above tar a bit and this is what i came up with. tar -cf - --exclude='./folder' --exclude='./file.tar' ./source_directory | tar -xf - -C ./destination_directory - Panduka

12
您可以使用带有 -prune 选项的 find 命令。
下面是来自 man find 的示例:
       cd /source-dir
       find . -name .snapshot -prune -o \( \! -name *~ -print0 \)|
       cpio -pmd0 /dest-dir

       这条命令将 /source-dir 目录中的内容复制到 /dest-dir 中,但省略了名为 .snapshot 的文件和目录(以及其中的所有内容)。它还省略了名称以 ~ 结尾的文件或目录,但不包括其内容。-prune -o \( ... -print0 \) 结构非常常见。这里的想法是,在 -prune 之前的表达式匹配要被修剪的内容。然而,-prune 动作本身返回真值,因此以下的 -o 确保右侧只对未被修剪的目录进行评估(已修剪的目录的内容甚至不会被访问,所以它们的内容是不相关的)。在 -o 右侧的表达式只是为了清晰起见加上的括号。它强调了 -print0 操作仅适用于没有应用 -prune 的事物。由于测试之间的默认“and”条件的绑定比 -o 更紧密,因此这是默认情况,但括号有助于显示正在进行的操作。 

赞扬你从 manpage 直接找到了一个高度相关的例子。 - David M
看起来确实不错!这也可以在在线文档中找到。不幸的是,cpio尚未为MSYS2打包。 - underscore_d

7

快速开始

运行:

rsync -av --exclude='path1/in/source' --exclude='path2/in/source' [source]/ [destination]

注释

  • -avr会创建一个名为[destination]的新目录。
  • sourcesource/会创建不同的结果:
    • source——将source的内容复制到destination中。
    • source/——将source文件夹复制到destination中。
  • 要排除多个文件:
    • --exclude-from=FILE——FILE是包含其他要排除的文件或目录名称的文件的名称。
  • --exclude也可以包含通配符:
    • 例如:--exclude=*/.svn*

修改自:https://dev59.com/3XI95IYBdhLWcg3wsQL9#2194500


示例

起始文件夹结构:

.
├── destination
└── source
    ├── fileToCopy.rtf
    └── fileToExclude.rtf

运行:

rsync -av --exclude='fileToCopy.rtf' source/ destination

最终的文件夹结构:

.
├── destination
│   └── fileToExclude.rtf
└── source
    ├── fileToCopy.rtf
    └── fileToExclude.rtf

1
【评论】布局不错,Jack。但是代码已经在10年前提交了:d所以尽管您发布了创意图标,我还是要点踩! - Goodies
1
@Goodies 当我阅读原始帖子时,我对布局和解释感到困惑,这就是为什么我重新格式化它的原因(感谢您欣赏图标和布局!)。如果我想改进原始答案,我应该尝试编辑它(十年前的那个)吗?我只是发现编辑需要一段时间才能获得批准,而我认为我的提交足够独特,可以被视为不同。 - Jack
没错,我会点赞你的评论。顺便说一下,我是一个初学者版主。我可以编辑东西,但我只会处理最近的标题。最好保持问题和答案的布局不变,除非真的有错误。 - Goodies

4

您可以使用tar命令,带上--exclude选项,然后在目标位置解压缩。例如:

cd /source_directory
tar cvf test.tar --exclude=dir_to_exclude *
mv test.tar /destination 
cd /destination  
tar xvf test.tar

请查看tar命令的man手册以获取更多信息。


3

与Jeff的想法类似(未经测试):

find . -name * -print0 | grep -v "exclude" | xargs -0 -I {} cp -a {} destination/

抱歉,我真的不明白为什么有5个人点赞这篇文章,因为它明显没有经过测试,在简单的测试中似乎也无法正常工作:我在/usr/share/icons的子目录中尝试了一下,立即出现了find: paths must precede expression: 22x22的错误,其中后者是其中一个子目录。我的命令是find . -name * -print0 | grep -v "scalable" | xargs -0 -I {} cp -a {} /z/test/(诚然,我在MSYS2上,所以实际上在/mingw64/share/icons/Adwaita,但我看不出这是MSYS2的问题)。 - underscore_d

2

简单解决方案(但我仍然更喜欢上面评论中的bash模式匹配方法):

touch /path/to/target/.git
cp -n -ax * /path/to/target/
rm /path/to/target/.git

这利用了cp-n选项,强制cp不覆盖现有目标。
缺点:仅适用于GNU cp。如果没有GNU cp,则cp操作可能会返回错误代码(1),这很烦人,因为你无法确定是否是真正的失败。

1

受 @SteveLazaridis 答案的启发,但会失败,这里提供一个 POSIX shell 函数 - 只需将其复制并粘贴到名为 cpx 的文件中,放在您的 $PATH 中并使其可执行 (chmod a+x cpr)。[源代码现在在我的 GitLab 上维护。]

#!/bin/sh

# usage: cpx [-n|--dry-run] "from_path" "to_path" "newline_separated_exclude_list"
# limitations: only excludes from "from_path", not it's subdirectories

cpx() {
# run in subshell to avoid collisions
  (_CopyWithExclude "$@")
}

_CopyWithExclude() {
  case "$1" in
    -n|--dry-run) { DryRun='echo'; shift; } ;;
  esac

  from="$1"
  to="$2"
  exclude="$3"

  $DryRun mkdir -p "$to"

  if [ -z "$exclude" ]; then
      cp "$from" "$to"
      return
  fi

  ls -A1 "$from" \
    | while IFS= read -r f; do
        unset excluded
        if [ -n "$exclude" ]; then
          for x in $(printf "$exclude"); do
          if [ "$f" = "$x" ]; then
              excluded=1
              break
          fi
          done
        fi
        f="${f#$from/}"
        if [ -z "$excluded" ]; then
          $DryRun cp -R "$f" "$to"
        else
          [ -n "$DryRun" ] && echo "skip '$f'"
        fi
      done
}

# Do not execute if being sourced
[ "${0#*cpx}" != "$0" ] && cpx "$@"

示例用法
EXCLUDE="
.git
my_secret_stuff
"
cpr "$HOME/my_stuff" "/media/usb" "$EXCLUDE"

如果没有解释答案的问题在哪里以及如何修复,仅仅说某人的答案“会失败”似乎没有帮助。 - underscore_d
@underscore_d: true,回想起来,尤其是现在我已经不记得哪里出了问题 :-( - go2null
多个问题:(1)它会多次复制文件,(2)逻辑仍会复制要排除的文件。使用i = foo运行循环:对于任何其他文件(例如i = test.txt),它将被复制3次,而不是4次。 - Eric Bringley
2
感谢 @EricBringley 澄清了 Steve 回答的不足之处。(虽然他确实说过它是“未经测试的”。) - go2null

0
EXCLUDE="foo bar blah jah"                                                                             
DEST=$1

for i in *
do
    for x in $EXCLUDE
    do  
        if [ $x != $i ]; then
            cp -a $i $DEST
        fi  
    done
done

未经测试...


2
这是不正确的。几个问题:按照现有的写法,会复制一个不应该被排除的文件多次(在这种情况下要排除的项目数量为4)。即使您尝试复制“foo”,即排除列表中的第一个项目,当您到达x=bar且i仍然是foo时,它仍将被复制。如果您坚持不使用预先存在的工具(例如rsync)进行此操作,请将复制移动到'for x in...'循环之外的if语句中,并使'for x...'循环更改if(true)复制文件的逻辑语句。这将防止您多次复制。 - Eric Bringley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接