grep 子文件夹文件并显示匹配的文件名

grep 子文件夹文件并显示匹配的文件名

我有一个独特的要求。文件夹内有大量子文件夹,子文件夹内有大量 CSV 文件。看起来像下面这样

SubfolderWB
>File1.csv
>File2.csv

SubfolderMUM
>File3.csv
>File4.csv
>file5.csv

SubfolderKEL
>File6.csv
>File7.csv

现在,在每个子文件夹中,我需要选择最后一个文件(或创建的最新文件)并使用 grep 与关键字匹配。如果关键字匹配我需要文件名。

示例:我需要在所有子文件夹的 CSV 文件中查找 foo。
所以我需要选择文件 cat SubfolderWB/File2.csv,SubfolderMUM/file5.csv ,SubfolderKEL/File7.csv | grep foo
如果 foo 存在于 file5.csv 中,它应该给我最终输出为 file5.csv。

答案1

你无法grep独自完成这件事。您至少需要使用find, 以及其他几个程序。

这是一种使用方法GNUfindstatsorttailcutxargsgrep和 的版本sed

find . -type f -iname '*.csv' -execdir sh -c '
    stat --printf "%Y\t$(pwd)/%n\0" "$@" |
      sort -z -n |
      tail -z -n 1 |
      cut -z -f2- |
      xargs -0r grep -l foo' sh {} + | sed 's=/\./=/='

对于包含一个或多个 .csv 文件的每个目录,find-execdir选项将更改为该目录并运行一个 shell 命令,该命令输出每个匹配文件名的完整路径的 NUL 分隔列表,每个文件名都以其修改时间戳和制表符为前缀。

然后该列表按数字排序,除了最近修改的文件名之外的所有文件名都被删除(按tail),时间戳来自cut输出,文件名通过管道传输到xargs运行中grep

最后,sed用于清理输出以删除字符串中/./嵌入的工件并将其替换为.这并不是绝对必要的,因为无论有没有路径名的工作方式都完全相同(Unix 根本不关心路径名的路径部分中的额外s 或s),但它看起来更好。$(pwd)/%nstat --printf//.//./


笔记:

  1. 如果需要,您可以使用find's-mindepth-maxdepth谓词来控制 find 如何递归搜索子目录。

  2. 此处既不使用grep也不sed生成 NUL 分隔的输出,因此如果任何文件名包含换行符,则在管道中使用是不“安全”的,但如果您只想在终端中显示文件名,则可以。为了安全地通过管道传输到其他程序,请将选项添加-Z到 grep 和-zsed...通过这两个更改,文件名列表将从头到尾以 NUL 分隔。

  3. 如果任何单个目录中的匹配文件名超过命令行长度限制(ARG_MAX,在 Linux 上约为 2MB),则此操作将无法正常工作,因为它将必须sh -c '...'对该目录运行多次,从而破坏所需的排序结果并添加文件名列表。这是值得注意的,但在实践中不太可能成为问题。

    同样,stat --printf扩展每个文件名以包含其完整路径,这可能会阻止stat成功运行......这更可能是一个问题,但在实践中仍然不太可能。它仍然需要有大量带有很长路径前缀的文件名才能超过 2MB ARG_MAX。

  4. 这是一种非常常用的技术的示例,通常称为“装饰-排序-取消装饰”或类似技术。很长一段时间以来,程序员一直在各种语言中使用它,至少从 lisp 刚刚诞生的时候开始。在这种情况下,find无法按时间戳排序,因此如果我们想这样做,我们需要添加时间戳以查找的输出(装饰),然后对其进行排序,然后删除时间戳(取消装饰)。


perl正如我在下面的评论之一中提到的,这也可以通过's来完成文件::查找IO::解压缩::AnyUncompress模块:

#!/usr/bin/perl

use File::Find;
use IO::Uncompress::AnyUncompress qw(anyuncompress $AnyUncompressError) ;
use Getopt::Std;
use strict;

my %files;   # hash-of-arrays to contain the filename with newest timestamp for each dir
my @matches; # array to contain filenames that contain the desired search pattern
my %opts;    # hash to contain command-line options

sub usage {
  print <<__EOF__;
$0 [-p 'search pattern'] [-f 'filename pattern'] [directory...]
-p and -f are required, and must have arguments.
directory defaults to current directory.
Example:
   $0 -p ABCD-713379 -f 'WB.*\.xml\.gz$' /data/inventory/ 
__EOF__
  exit 1
};

# Extremely primitive option processing and error checking.
usage unless getopts('p:f:', \%opts) && $opts{p} && $opts{f};

# default to current directory if not supplied.
@ARGV = qw(./) unless @ARGV;

# Find the newest filename in each subdirectory
find(\&wanted, @ARGV);

# OK, we should now have a %files hash where the keys are the
# directory names, and the values are an array containing a
# timestamp and the newest filename in that directory.
#
# Now "grep" each of those files by reading in each
# line and seeing if it contains the search pattern.
# IO::Uncompress::AnyUncompress ensures this works with
# compressed and uncompressed files.  Works with most common
# compression formats.
# The `map ...` extracts only the filenames from %files - see "perldoc -f map"
foreach my $f (map { $files{$_}[1] } keys %files) {
  my $z = IO::Uncompress::AnyUncompress->new($f) or
    warn "anyuncompress failed for '$f': $AnyUncompressError\n";

  while (my $line = $z->getline()) {
    if ($line =~ m/$opts{p}/i) { push @matches, $f ; last };
  };
};

# Output the list of matching filenames, separated by newlines.
print join("\n",@matches), "\n";
#print join("\0",@matches), "\0";  # alternatively, NUL-separated filenames

# "wanted()" subroutine used by File::Find to match files
sub wanted {
  # ignore directories, symlinks, etc and files that don't
  # match the filename pattern.
  return unless (-f && /$opts{f}/i);

  # Is this the first file we've seen in this dir? Is the current
  # file newer than the one we've already seen?
  # If either is true, store it in %files.
  my $t = (stat($File::Find::name))[9];
  if (!defined $files{$File::Find::dir} || $t > $files{$File::Find::dir}[0]) {
    $files{$File::Find::dir} = [ $t, $File::Find::name ]
  };
};

忽略注释,这大约有 35 行代码。其中大部分都是样板文件。编写注释比编写代码花费的时间更长,因为大部分注释只是从模块的手册页或我之前编写的类似脚本中复制粘贴和编辑的。

运行它,例如,./find-and-grep.pl -f '\.csv$' -p foo ./.

或者./find-and-grep.pl -p ABCD-713379 -f 'WB.*\.xml\.gz$' /data/inventory/

答案2

给定一组包含文件的子目录

 % tree -tD --timefmt='%H:%M:%S'
.
├── [07:46:40]  SubfolderKEL
│   ├── [07:46:20]  File1
│   ├── [07:46:24]  File3
│   ├── [07:46:26]  File4
│   ├── [07:46:30]  File6
│   ├── [07:46:32]  File7
│   ├── [07:46:34]  File8
│   ├── [07:46:36]  File9
│   └── [08:05:32]  File11
├── [07:46:54]  SubfolderWB
│   ├── [07:46:38]  File10
│   ├── [07:46:48]  File15
│   ├── [07:46:52]  File17
│   └── [07:46:54]  File18
└── [07:46:58]  SubfolderMUM
    ├── [07:46:22]  File2
    ├── [07:46:28]  File5
    ├── [07:46:42]  File12
    ├── [07:46:44]  File13
    ├── [07:46:46]  File14
    ├── [07:46:50]  File16
    ├── [07:46:56]  File19
    └── [07:46:58]  File20

3 directories, 20 files

然后使用zsh,您可以在匿名函数中使用 glob 限定符从每个子目录中选择最新的文件(按修改时间):

 % for d (Subfolder*(/)) (){ print -rC1 $@ } $d/*(om[1])
SubfolderKEL/File11
SubfolderMUM/File20
SubfolderWB/File18

使用相同的结构,您可以grep获取内容并返回包含匹配项的文件的名称:

 % for d (Subfolder*(/)) (){ grep -l foo -- $@ } $d/*(om[1])
SubfolderKEL/File11

答案3

假设您对文件修改时间而不是创建时间(Unix 不保留)感到满意,然后使用 GNU findsortawk

#!/usr/bin/env bash

find . -type f -name '*.csv' -printf '%T@ %p\0' |
sort -srnz |
awk -v RS='\0' '
    ARGIND == 1 {
        match($0,"[^ ]+ ((.*)/[^/]+$)",a)
        if ( !seen[a[2]]++ ) {
            ARGV[ARGC++] = a[1]
        }
    }
    /foo/ {
        print FILENAME
        nextfile
    }
' -

答案4

以下使用zshshell 并假设变量pattern包含您想要匹配的基本正则表达式。

for dirpath in Subfolder*(/); do
    grep -l -e $pattern $dirpath/*.csv(.om[1])
done

for循环迭代当前目录中名称以Subfolder.对于每个这样的目录,最近修改的常规文件(其名称与模式匹配)*.csv被赋予grep。该grep实用程序将尝试匹配给定的正则表达式,如果匹配,它将打印文件的名称(包括子目录名称)。

这里使用的特殊zsh功能是两个全局限定符(/)(.om[1])。第一个使前面的模式仅匹配目录,而第二个使模式仅匹配常规文件并按修改时间戳对文件进行排序,并仅返回排序项中的第一个(即最近修改的常规文件)。

-l选项grep使其仅输出匹配的文件的路径名。

相关内容