Bash函数:查找最新的与模式匹配的文件

181
在Bash中,我想创建一个函数,返回符合某个特定模式的最新文件的文件名。例如,我有一个文件目录如下:
Directory/
   a1.1_5_1
   a1.2_1_4
   b2.1_0
   b2.2_3_4
   b2.3_2_0

我想要获取以'b2'开头的最新文件。在bash中应该怎么做?我需要将这个命令写进我的~/.bash_profile脚本中。


4
请参考此链接:http://superuser.com/questions/294161/unix-linux-find-and-sort-by-date-modified,获取更多答案提示。排序是获取最新文件的关键步骤。 - Wolfgang Fahl
10个回答

301
ls 命令有一个参数 -t 可以按时间排序。然后你可以用 head -1 来获取第一个(最新的)。
ls -t b2* | head -1

但要小心:为什么不应该解析ls的输出 我个人的观点是,当文件名可能包含空格或换行符等特殊字符时,解析ls是很危险的。
如果你能保证文件名不会包含特殊字符(也许是因为你控制文件的生成方式),那么解析ls是相当安全的。
如果你正在开发一个脚本,该脚本将在许多不同情况下由许多人在许多系统上运行,请不要解析ls。
以下是安全的做法:
unset -v latest
for file in "$dir"/*; do
  [[ $file -nt $latest ]] && latest=$file
done

要了解更多,请阅读如何在目录中找到最新(最新,最早,最旧)的文件?

还有测试、单方括号和双方括号之间的区别是什么?


12
提示:如果你是在为目录做这个操作,你需要在 ls 命令中添加 -d 选项,像这样 'ls -td <pattern> | head -1'。 - ken.ganong
6
parsing LS链接表示不要这样做,并推荐在BashFAQ 99中提供的方法。我正在寻找一个一行代码而不是一个弹性脚本中的东西,所以我会像@lesmana一样不安全地解析ls结果。 - Eponymous
1
@Eponymous:如果你想要一个不使用脆弱的 ls 的一行代码,那么 printf "%s\n" b2* | head -1 就可以满足你的需求。 - David Ongaro
3
@DavidOngaro 这个问题并没有说文件名是版本号。这是关于修改时间的。即使有文件名的假设 b2.10_5_2 也会破坏这个解决方案。 - Eponymous
1
你的一行代码给了我正确的答案,但“正确”的方法实际上给了我最老的文件。有任何想法为什么? - NewNameStat
显示剩余11条评论

34
findls的组合适用于以下情况:
  • 没有换行符的文件名
  • 文件数量不是很大
  • 文件名不是很长
解决方案:
find . -name "my-pattern" -print0 |
    xargs -r -0 ls -1 -t |
    head -1

让我们来分解一下:

使用find命令,我们可以像这样匹配所有感兴趣的文件:

find . -name "my-pattern" ...

然后使用-print0,我们可以安全地将所有文件名传递给ls,像这样:

find . -name "my-pattern" -print0 | xargs -r -0 ls -1 -t

可以在此处添加其他find搜索参数和模式

find . -name "my-pattern" ... -print0 | xargs -r -0 ls -1 -t

ls -t 命令会按修改时间排序文件(最新的在前面),每行打印一个文件名。你可以使用 -c 选项按创建时间排序。注意:如果文件名中包含换行符,该命令会出错。

最后,head -1 命令获取排序后列表中的第一个文件。

注意:xargs 命令使用系统限制来控制参数列表的大小。如果列表大小超过限制,xargs 将多次调用 ls 命令。这将破坏排序,可能也会破坏最终输出结果。请运行

xargs  --show-limits

检查您系统的限制。

注2:如果您不想通过子文件夹搜索文件,请使用find . -maxdepth 1 -name "my-pattern" -print0

注3:正如@starfry所指出的那样,xargs-r参数将防止调用ls -1 -t,如果find没有匹配到任何文件。感谢您的建议。


2
这比基于ls的解决方案更好,因为它适用于具有大量文件的目录,而ls则会出现问题。 - Marcin Zukowski
find . -name "my-pattern" ... -print0 gives me find: paths must precede expression: \...'` - Jaakko
1
哦!...代表“更多参数”。如果你不需要它,就省略掉。 - Boris Brodski
3
如果没有与模式匹配的文件,find命令有可能返回一个不符合模式的文件。这是因为find没有将任何内容传递给xargs,进而导致xargs没有文件列表来调用ls命令,从而处理所有文件。解决方法是在xargs命令行中添加"-r"选项,告诉xargs如果在标准输入中没有接收到任何内容,则不要运行其命令行。 - starfry
@starfry 谢谢!很好的发现。我在答案中添加了“-r”。 - Boris Brodski
显示剩余6条评论

12

这是所需 Bash 函数的可能实现:

# Print the newest file, if any, matching the given pattern
# Example usage:
#   newest_matching_file 'b2*'
# WARNING: Files whose names begin with a dot will not be checked
function newest_matching_file
{
    # Use ${1-} instead of $1 in case 'nounset' is set
    local -r glob_pattern=${1-}

    if (( $# != 1 )) ; then
        echo 'usage: newest_matching_file GLOB_PATTERN' >&2
        return 1
    fi

    # To avoid printing garbage if no files match the pattern, set
    # 'nullglob' if necessary
    local -i need_to_unset_nullglob=0
    if [[ ":$BASHOPTS:" != *:nullglob:* ]] ; then
        shopt -s nullglob
        need_to_unset_nullglob=1
    fi

    newest_file=
    for file in $glob_pattern ; do
        [[ -z $newest_file || $file -nt $newest_file ]] \
            && newest_file=$file
    done

    # To avoid unexpected behaviour elsewhere, unset nullglob if it was
    # set by this function
    (( need_to_unset_nullglob )) && shopt -u nullglob

    # Use printf instead of echo in case the file name begins with '-'
    [[ -n $newest_file ]] && printf '%s\n' "$newest_file"

    return 0
}

它仅使用Bash内置命令,并且应该能够处理文件名包含换行符或其他不寻常字符的文件。


1
你可以使用 nullglob_shopt=$(shopt -p nullglob),然后稍后使用 $nullglobnullglob 恢复到先前的状态。 - gniourf_gniourf
@gniourf_gniourf 的建议使用 $(shopt -p nullglob) 是一个好主意。我通常尽量避免使用命令替换($() 或反引号),因为它很慢,特别是在 Cygwin 下,即使命令只使用内置命令。此外,命令运行的子 shell 上下文有时会导致它们以意想不到的方式运行。我也尽量避免将命令存储在变量中(如 nullglob_shopt),因为如果变量值错误,可能会发生非常糟糕的事情。 - pjh
我很欣赏注重细节的态度,因为忽视它们可能会导致难以察觉的失败。谢谢! - Ron Burk
我喜欢你采用了一种更独特的方式来解决问题!在Unix/Linux中,有多种方法可以“剥皮**”!即使这需要更多的工作,它也有展示概念的好处。加一分! - Pryftan

9

使用find命令。

假设您正在使用Bash 4.2+,可以使用-printf '%T+ %p\n'来获取文件的时间戳值。

find $DIR -type f -printf '%T+ %p\n' | sort -r | head -n 1 | cut -d' ' -f2

例子:

find ~/Downloads -type f -printf '%T+ %p\n' | sort -r | head -n 1 | cut -d' ' -f2

如需更实用的脚本,请查看此处的find-latest脚本:https://github.com/l3x/helpers


要处理包含空格的文件名,请更改 cut -d' ' -f2,3,4,5,6,7,8,9 ... - valodzka
2
Bash的版本并不重要。您需要拥有GNUfind,因为-printf选项是非标准的(通常情况下,这仅适用于Linux操作系统)。 - tripleee

6
你可以使用stat和文件通配符一起使用,通过装饰-排序-去装饰的方式,在文件时间前面添加它:
$ stat -f "%m%t%N" b2* | sort -rn | head -1 | cut -f2-

正如评论中所述,最好的跨平台解决方案可能是使用Python、Perl或Ruby脚本。

在这种情况下,我倾向于使用Ruby,因为它非常类似于awk,可以轻松编写小型且可随意删除的脚本,同时具有从命令行中读取Python或Perl的强大功能。

以下是一个Ruby脚本的示例:

ruby -e '
# index [0] for oldest and [-1] for newest
newest=Dir.glob("*").
    reject { |f| File.directory?(f)}.
    sort_by { |f| File.birthtime(f) rescue File.mtime(f) 
    }[-1]
p newest'

获取当前工作目录中最新的文件。

您还可以通过在glob中使用** / *进行递归,或使用b2*等限制匹配文件。


无法。"stat: 无法读取文件系统信息 '%m%t%N':没有这样的文件或目录" - Ken Ingram
3
我认为这可能适用于stat的Mac/FreeBSD版本,如果我正确记得它的选项。要在其他平台上获得类似的输出,您可以使用stat -c $'%Y\t%n' b2* | sort -rn | head -n1 | cut -f2- - Jeffrey Cash
1
“其他平台”可能指的是Linux。仍然有其他平台需要不同的选项,或者在最坏的情况下,无法轻松地提供对stat行为的这种粒度级别的控制。如果您需要一种便携式解决方案,具有讽刺意味的是,也许编写一个Perl或Python脚本会更好。 - tripleee

6
一个用于在匹配模式的目录下查找最新文件的Bash函数
#1.  Make a bash function:
newest_file_matching_pattern(){ 
    find $1 -name "$2" -print0 | xargs -0 ls -1 -t | head -1  
} 
 
#2. Setup a scratch testing directory: 
mkdir /tmp/files_to_move;
cd /tmp/files_to_move;
touch file1.txt;
touch file2.txt; 
touch foobar.txt; 
 
#3. invoke the function: 
result=$(newest_file_matching_pattern /tmp/files_to_move "file*") 
printf "result: $result\n"

输出:

result: /tmp/files_to_move/file2.txt

如果您更喜欢使用 Python 解释器,那么以下命令也能实现同样的功能:

#!/bin/bash 
 
function newest_file_matching_pattern { 
python - <<END 
import glob, os, re  
print(sorted(glob.glob("/tmp/files_to_move/file*"), key=os.path.getmtime)[0]); 
END 
} 
 
result=$(newest_file_matching_pattern) 
printf "result: $result\n" 

输出:

result: /tmp/files_to_move/file2.txt

这是“破碎的引用”和“无用的echo使用”,但对于大目录来说更是“破碎的”。 - tripleee
@tripleee 所有出色的Bash技巧和链接。将三年前的可怕代码变得更好了一些。 - Eric Leschinski

4

不寻常的文件名(例如包含有效的\n字符的文件)可能会对此类解析造成严重影响。以下是使用Perl的一种方法:

perl -le '@sorted = map {$_->[0]} 
                    sort {$a->[1] <=> $b->[1]} 
                    map {[$_, -M $_]} 
                    @ARGV;
          print $sorted[0]
' b2*

这里使用了Schwartzian变换


1
愿 schwartz 与你同在! - Nathan Monteleone
这个答案可能有效,但是考虑到文档质量较差,我不太信任它。 - Wolfgang Fahl

1

0

结合findstatsortcuttail

  1. find命令查找文件-type f(匹配名称-name 'b2*'
  2. xargs stat命令对这些文件进行统计,打印出自纪元以来的%Y秒和文件名%n
  3. sort命令对其进行排序
  4. cut命令从第2个字段开始截取-f 2 onwards 2-(以制表符分隔)
  5. tail命令获取排序后的最新文件的最后一个-n 1

适用于所有的shell和POSIX兼容系统(据我所知)

tab=$(printf '\t');
find . -type f -print0 |
  xargs -0 stat --format "%Y$tab%n" |
  sort |
  cut -f 2- |
  tail -n 1

请随意使用"$tab"来代替一个实际的制表符,这在SO上不起作用。

OP要求过滤以b2开头的文件名,所以应该是

tab=$(printf '\t');
find . -type f -name 'b2*' -print0 |
  xargs -0 stat --format "%Y$tab%n" |
  sort |
  cut -f 2- |
  tail -n 1

-2

有一种更高效的方法可以实现这个目标。请考虑以下命令:

find . -cmin 1 -name "b2*"

这个命令使用通配符搜索"b2*",找到刚好一分钟前生成的最新文件。如果你想要过去两天内的文件,最好使用以下命令:

find . -mtime 2 -name "b2*"

"

“.”代表当前目录。 希望这可以帮到你。

"

10
实际上,这并没有找到“匹配模式的最新文件”... 它只是找到了一分钟前创建或两天前修改的所有匹配模式的文件。 - GnP
这个答案是基于所提出的问题。此外,您可以调整命令以查看一两天前最新的文件。这取决于您想要做什么。 - Naufal
“微调”不是答案。这就像把这个作为答案发布:“只需微调查找命令并根据您想要做什么找到答案”。 - Kennet Celeste
不确定关于不必要的评论。如果你觉得我的回答没有证实,那么请提供充分的理由,并举例说明为什么我的回答没有意义。如果无法这样做,请不要再发表评论。 - Naufal
2
你的解决方案需要你知道最新文件创建的时间。这不是问题中提出的,所以你的答案并不基于问题本身。 - Bloke Down The Pub

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