在Linux中如何查找同一目录下名称相同但大小写不同的重复文件?

35

如何返回同名但大小写不同的文件列表,这些文件位于同一目录中?

我不关心文件的内容,只需要知道任何具有相同名称副本的文件的位置和名称。

重复文件示例:

/www/images/taxi.jpg
/www/images/Taxi.jpg
理想情况下,我需要从一个基础目录递归搜索所有文件。在上面的例子中,这个基础目录是/www/

如果您有相同的名称,但都是小写字母,并且位于不同的文件夹中,您会删除哪一个? - ghostdog74
1
@ghost:但是在同一文件夹中存在不同的情况。 - paxdiablo
正如 @paxdiablo 所指出的,我只关心存在于同一文件夹中的具有相同名称的重复项。 - Camsoft
但是你说理想情况下需要递归搜索?还是我漏掉了什么? - ghostdog74
@ghostdog74 我想要查找同一文件夹中已有的重复命名文件。但我想要在每个文件夹和子文件夹中执行新的重复文件搜索。我基本上是在寻找整个文件系统树中在同一位置上重复的任何文件。 - Camsoft
1
@Camsoft,请重新考虑您想要接受哪个答案。@Christoffer Hammarström的答案比我的更优雅,而且完全实现了相同的功能。 - paxdiablo
11个回答

44
另一个答案很好,但我建议使用“相对庞大的”Perl脚本的代替。
perl -pe 's!([^/]+)$!lc $1!e'

这将仅将路径中的文件名部分转换为小写。

编辑1:实际上,整个问题可以通过以下方式解决:

find . | perl -ne 's!([^/]+)$!lc $1!e; print if 1 == $seen{$_}++'

编辑3:我找到了一种使用sed,sort和uniq的解决方案,它还会打印出重复项,但只有在文件名中没有空格时才有效。
find . |sed 's,\(.*\)/\(.*\)$,\1/\2\t\1/\L\2,'|sort|uniq -D -f 1|cut -f 1

编辑2: 这是一个更长的脚本,用于打印名称,它从标准输入中获取路径列表,如find所示。不太优雅,但仍有效:
#!/usr/bin/perl -w

use strict;
use warnings;

my %dup_series_per_dir;
while (<>) {
    my ($dir, $file) = m!(.*/)?([^/]+?)$!;
    push @{$dup_series_per_dir{$dir||'./'}{lc $file}}, $file;
}

for my $dir (sort keys %dup_series_per_dir) {
    my @all_dup_series_in_dir = grep { @{$_} > 1 } values %{$dup_series_per_dir{$dir}};
    for my $one_dup_series (@all_dup_series_in_dir) {
        print "$dir\{" . join(',', sort @{$one_dup_series}) . "}\n";
    }
}

5
我强烈建议接受“这个答案”(而不是当前被接受的答案)。它更加优雅。我的最终版本非常丑陋,因为我从管道视角来看待它,并不得不添加Perl以解决tr的问题。这个答案证明了通常通过退后一步重新开始可以得到更好的解决方案。 - paxdiablo
2
如果你想限制到常规文件(不包括目录),请使用“find -type f”。 - paxdiablo
如果脚本能够同时显示两个有问题的文件,那将非常有用。 - Soviut
如果您有多个子目录需要查找具有相同名称的文件:find . |xargs -n 1 basename| perl -ne 's!([^/]+)$!lc $1!e; print if 1 == $seen{$_}++' - Peter
1
@PeterSenna:或者只需 find . | perl -ne 's!.*/([^/]+)!lc $1!se; print if 1 == $seen{$_}++' - Christoffer Hammarström
显示剩余2条评论

38

尝试:

ls -1 | tr '[A-Z]' '[a-z]' | sort | uniq -c | grep -v " 1 "

简单明了:)管道真是太神奇了!

ls -1 命令可以让文件每行一个显示,tr '[A-Z]' '[a-z]' 命令将所有大写字母转换为小写字母,sort 命令按顺序排列它们(出人意料吧),uniq -c 命令删除重复行并同时给您计数,最后,grep -v " 1 " 命令剥离掉出现次数为1的行。

当我在含有一个"重复"文件的目录中运行此命令时(我将"qq"复制到"qQ"中),我得到:

2 qq

对于“当前目录及其所有子目录”版本,只需将 ls -1 替换为 find .find DIRNAME(如果您想要指定目录作为起始点,则使用 DIRNAME 作为您要使用的目录名称)。

这会返回以下结果(适用于我):

2 ./.gconf/system/gstreamer/0.10/audio/profiles/mp3
2 ./.gconf/system/gstreamer/0.10/audio/profiles/mp3/%gconf.xml
2 ./.gnome2/accels/blackjack
2 ./qq

这些原因包括:

pax> ls -1d .gnome2/accels/[bB]* .gconf/system/gstreamer/0.10/audio/profiles/[mM]* [qQ]?
.gconf/system/gstreamer/0.10/audio/profiles/mp3
.gconf/system/gstreamer/0.10/audio/profiles/MP3
.gnome2/accels/blackjack
.gnome2/accels/Blackjack
qq
qQ

更新:

事实上,在进一步思考后,tr将会将路径的所有组件转换为小写字母,因此以下两种写法会被视为相同:

/a/b/c
/a/B/c

即使它们在不同的目录中,也将被视为重复。

如果您只想在单个目录内显示重复项作为匹配项,则可以使用(相当庞大的):

perl -ne '
    chomp;
    @flds = split (/\//);
    $lstf = $f[-1];
    $lstf =~ tr/A-Z/a-z/;
    for ($i =0; $i ne $#flds; $i++) {
        print "$f[$i]/";
    };
    print "$x\n";'

替代方式:

tr '[A-Z]' '[a-z]'

它所做的只是将路径名的最后一部分转换为小写,而不是整个路径名。此外,如果您只想要普通文件(没有目录、FIFO等),请使用find -type f来限制返回的内容。


你可以在Windows上轻松地完成这个任务。获取Cygwin或MinGW的副本并享受吧 :-) - paxdiablo
但是你不能直接做到这一点。 - Camsoft
2
很棒的回答,但是有一个小优化建议:我认为如果你将其重定向到管道中,ls上不需要“-1”。 - Carl Smotricz
如果有多个匹配文件的文件名包含“1”,则应使用grep -v "^ *1" - Dennis Williamson
1
这个解决方案对我来说非常有效,谢谢!不过我不得不稍微改进一下。我使用了find命令,但似乎目录路径导致重复项无法被正确找到,因此我在命令中添加了另一个管道,使用awk从找到的记录中删除目录路径,所以我的最终命令看起来是这样的:find . -type f | awk -F/ '{print $NF}' | tr '[A-Z]' '[a-z]' | sort | uniq -c | grep -v " 1 " 请注意,这只会列出重复的文件名,而不是它们的目录,并且您将无法使用管道来删除它们等。 - SimonDowdles
显示剩余7条评论

6

我相信

ls | sort -f | uniq -i -d

更简单、更快并且能够得到相同的结果。

是的,对于当前目录。但是子目录呢?请注意,您只能忽略基本名称的大小写,而不能忽略整个路径。 - Christoffer Hammarström
在Mac OSX上,如果您有非ASCII字符,则可能需要使用export LC_ALL='C'设置字符编码。 - Turadg
对于子目录,请在ls命令中添加-R开关。 - k.honsali

4

针对mpez0的回复,如果要进行递归检测,只需将“ls”替换为“find .”即可。

我唯一看到的问题是,如果这是一个正在复制的目录,则每个文件在此目录中都有1个条目。需要人工处理输出结果。

但无论如何,你不会自动删除这些文件,对吧?

find . | sort -f | uniq -i -d

这可能更像是一条评论而不是答案,似乎在询问澄清问题。 - vgoff

2
这是一个不错的小型命令行应用程序,名为findsn,您可以通过编译fslint获得,而deb包中没有包含它。
它可以找到任何同名文件,并且速度非常快,可以处理不同大小写。
/findsn --help
find (files) with duplicate or conflicting names.
Usage: findsn [-A -c -C] [[-r] [-f] paths(s) ...]

如果没有提供参数,则会搜索$PATH以查找任何冗余或冲突的文件。
-A  reports all aliases (soft and hard links) to files.
    If no path(s) specified then the $PATH is searched.

如果只指定了路径,则会检查是否存在重名文件。您可以使用-C来忽略此搜索中的大小写。使用-c进行限定更加严格,因为仅报告名称仅在大小写上不同的同一目录中的文件(或目录)。即-c将标记文件和目录,如果传输到不区分大小写的文件系统中将产生冲突。请注意,如果指定了-c或-C并且未指定路径,则假定当前目录。


2

以下是如何查找所有重复的jar文件的示例:

find . -type f -printf "%f\n" -name "*.jar" | sort -f | uniq -i -d

*.jar替换为您要查找的任何重复文件类型。


1

您可以使用:

find -type f  -exec readlink -m {} \; | gawk 'BEGIN{FS="/";OFS="/"}{$NF=tolower($NF);print}' | uniq -c

何时使用:

  • find -type f
    递归打印所有文件的完整路径。

  • -exec readlink -m {} \;
    获取文件的绝对路径。

  • gawk 'BEGIN{FS="/";OFS="/"}{$NF=tolower($NF);print}'
    将所有文件名替换为小写字母。

  • uniq -c
    去重路径,-c 输出重复计数。


1

这是我使用过的一个脚本(我不是作者)。原始脚本和讨论可以在这里找到: http://www.daemonforums.org/showthread.php?t=4661

#! /bin/sh

# find duplicated files in directory tree
# comparing by file NAME, SIZE or MD5 checksum
# --------------------------------------------
# LICENSE(s): BSD / CDDL
# --------------------------------------------
# vermaden [AT] interia [DOT] pl
# http://strony.toya.net.pl/~vermaden/links.htm

__usage() {
  echo "usage: $( basename ${0} ) OPTION DIRECTORY"
  echo "  OPTIONS: -n   check by name (fast)"
  echo "           -s   check by size (medium)"
  echo "           -m   check by md5  (slow)"
  echo "           -N   same as '-n' but with delete instructions printed"
  echo "           -S   same as '-s' but with delete instructions printed"
  echo "           -M   same as '-m' but with delete instructions printed"
  echo "  EXAMPLE: $( basename ${0} ) -s /mnt"
  exit 1
  }

__prefix() {
  case $( id -u ) in
    (0) PREFIX="rm -rf" ;;
    (*) case $( uname ) in
          (SunOS) PREFIX="pfexec rm -rf" ;;
          (*)     PREFIX="sudo rm -rf"   ;;
        esac
        ;;
  esac
  }

__crossplatform() {
  case $( uname ) in
    (FreeBSD)
      MD5="md5 -r"
      STAT="stat -f %z"
      ;;
    (Linux)
      MD5="md5sum"
      STAT="stat -c %s"
      ;;
    (SunOS)
      echo "INFO: supported systems: FreeBSD Linux"
      echo
      echo "Porting to Solaris/OpenSolaris"
      echo "  -- provide values for MD5/STAT in '$( basename ${0} ):__crossplatform()'"
      echo "  -- use digest(1) instead for md5 sum calculation"
      echo "       $ digest -a md5 file"
      echo "  -- pfexec(1) is already used in '$( basename ${0} ):__prefix()'"
      echo
      exit 1
    (*)
      echo "INFO: supported systems: FreeBSD Linux"
      exit 1
      ;;
  esac
  }

__md5() {
  __crossplatform
  :> ${DUPLICATES_FILE}
  DATA=$( find "${1}" -type f -exec ${MD5} {} ';' | sort -n )
  echo "${DATA}" \
    | awk '{print $1}' \
    | uniq -c \
    | while read LINE
      do
        COUNT=$( echo ${LINE} | awk '{print $1}' )
        [ ${COUNT} -eq 1 ] && continue
        SUM=$( echo ${LINE} | awk '{print $2}' )
        echo "${DATA}" | grep ${SUM} >> ${DUPLICATES_FILE}
      done

  echo "${DATA}" \
    | awk '{print $1}' \
    | sort -n \
    | uniq -c \
    | while read LINE
      do
        COUNT=$( echo ${LINE} | awk '{print $1}' )
        [ ${COUNT} -eq 1 ] && continue
        SUM=$( echo ${LINE} | awk '{print $2}' )
        echo "count: ${COUNT} | md5: ${SUM}"
        grep ${SUM} ${DUPLICATES_FILE} \
          | cut -d ' ' -f 2-10000 2> /dev/null \
          | while read LINE
            do
              if [ -n "${PREFIX}" ]
              then
                echo "  ${PREFIX} \"${LINE}\""
              else
                echo "  ${LINE}"
              fi
            done
        echo
      done
  rm -rf ${DUPLICATES_FILE}
  }

__size() {
  __crossplatform
  find "${1}" -type f -exec ${STAT} {} ';' \
    | sort -n \
    | uniq -c \
    | while read LINE
      do
        COUNT=$( echo ${LINE} | awk '{print $1}' )
        [ ${COUNT} -eq 1 ] && continue
        SIZE=$( echo ${LINE} | awk '{print $2}' )
        SIZE_KB=$( echo ${SIZE} / 1024 | bc )
        echo "count: ${COUNT} | size: ${SIZE_KB}KB (${SIZE} bytes)"
        if [ -n "${PREFIX}" ]
        then
          find ${1} -type f -size ${SIZE}c -exec echo "  ${PREFIX} \"{}\"" ';'
        else
          # find ${1} -type f -size ${SIZE}c -exec echo "  {}  " ';'  -exec du -h "  {}" ';'
          find ${1} -type f -size ${SIZE}c -exec echo "  {}  " ';'
        fi
        echo
      done
  }

__file() {
  __crossplatform
  find "${1}" -type f \
    | xargs -n 1 basename 2> /dev/null \
    | tr '[A-Z]' '[a-z]' \
    | sort -n \
    | uniq -c \
    | sort -n -r \
    | while read LINE
      do
        COUNT=$( echo ${LINE} | awk '{print $1}' )
        [ ${COUNT} -eq 1 ] && break
        FILE=$( echo ${LINE} | cut -d ' ' -f 2-10000 2> /dev/null )
        echo "count: ${COUNT} | file: ${FILE}"
        FILE=$( echo ${FILE} | sed -e s/'\['/'\\\['/g -e s/'\]'/'\\\]'/g )
        if [ -n "${PREFIX}" ]
        then
          find ${1} -iname "${FILE}" -exec echo "  ${PREFIX} \"{}\"" ';'
        else
          find ${1} -iname "${FILE}" -exec echo "  {}" ';'
        fi
        echo
      done 
  }

# main()

[ ${#} -ne 2  ] && __usage
[ ! -d "${2}" ] && __usage

DUPLICATES_FILE="/tmp/$( basename ${0} )_DUPLICATES_FILE.tmp"

case ${1} in
  (-n)           __file "${2}" ;;
  (-m)           __md5  "${2}" ;;
  (-s)           __size "${2}" ;;
  (-N) __prefix; __file "${2}" ;;
  (-M) __prefix; __md5  "${2}" ;;
  (-S) __prefix; __size "${2}" ;;
  (*)  __usage ;;
esac

如果您发现find命令无法正常工作,您可能需要进行更改。例如:
OLD :   find "${1}" -type f | xargs -n 1 basename 
NEW :   find "${1}" -type f -printf "%f\n"

0
有点晚了,但这是我选择的版本:
find . -type f | awk -F/ '{print $NF}' | sort -f | uniq -i -d

这里我们使用了:

  1. find - 查找当前目录下的所有文件
  2. awk - 移除文件名中的文件路径部分
  3. sort - 不区分大小写地排序
  4. uniq - 从通过管道传递的内容中找到重复项

(受 @mpez0 的回答和 @SimonDowdles 对 @paxdiablo 回答的评论启发。)


0

你可以使用GNU awk在给定的目录中检查重复项:

gawk 'BEGINFILE {if ((seen[tolower(FILENAME)]++)) print FILENAME; nextfile}' *

这里使用 BEGINFILE 在读取文件之前执行某些操作。在本例中,它通过一个数组 seen[] 来跟踪出现过的名称,其索引是小写的文件名。

如果一个名称已经出现过,无论大小写如何,它都会将其打印出来。否则,它只会跳到下一个文件。


看一个例子:
$ tree
.
├── bye.txt
├── hello.txt
├── helLo.txt
├── yeah.txt
└── YEAH.txt

0 directories, 5 files
$ gawk 'BEGINFILE {if ((a[tolower(FILENAME)]++)) print FILENAME; nextfile}' *
helLo.txt
YEAH.txt

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