是否有一个bash命令来计数文件?

是否有一个bash命令来计算匹配模式的文件数量?

例如,我想获取一个目录中所有文件的计数,这些文件匹配这个模式:log*

170567 次浏览

对于递归搜索:

find . -type f -name '*.log' -printf x | wc -c

wc -c将计算find输出中的字符数,而-printf x告诉find为每个结果打印一个x。这避免了包含换行符等奇怪名称的文件的任何问题。

对于非递归搜索,这样做:

find . -maxdepth 1 -type f -name '*.log' -printf x | wc -c

这个简单的一行代码可以在任何shell中工作,而不仅仅是bash:

ls -1q log* | wc -l

Ls -1q将为每个文件提供一行,即使它们包含空格或换行符等特殊字符。

输出被输送到wc -l,它计算行数。

你可以使用bash安全地做到这一点(即不会被名称中带有空格或\n的文件所bug):

$ shopt -s nullglob
$ logfiles=(*.log)
$ echo ${#logfiles[@]}

你需要启用nullglob,这样如果没有文件匹配,你就不会在$logfiles 数组中得到字面值*.log。(关于如何安全重置它的例子,请参见如何“撤销”一个'set -x'?。)

ls -1 log* | wc -l

这意味着每行列出一个文件,然后将其输送到单词计数命令,参数切换到计数行。

如果你有很多文件,你不想使用优雅的shopt -s nullglob和bash数组解决方案,你可以使用find等,只要你不打印出文件名(可能包含换行符)。

find -maxdepth 1 -name "log*" -not -name ".*" -printf '%i\n' | wc -l

这将找到所有匹配log*且不以.*开头的文件-“not name .*”是冗余的,但重要的是要注意,“ls”的默认值是不显示点文件,但find的默认值是包括它们。

这是一个正确答案,可以处理任何类型的文件名,因为文件名永远不会在命令之间传递。

但是,shopt nullglob答案是最好的答案!

这个问题的公认答案是错误的,但我有低代表,所以不能添加评论。

这个问题的正确答案由Mat给出:

shopt -s nullglob
logfiles=(*.log)
echo ${#logfiles[@]}

接受的答案的问题是wc -l计算换行符的数量,即使它们打印到终端,也将它们计算为'?'ls -l'的输出。这意味着当文件名包含换行符时,接受的答案失败。我已经测试了建议的命令:

ls -l log* | wc -l

而且即使只有一个文件名恰好包含换行符的文件与模式匹配,它也会错误地报告值2。例如:

touch log$'\n'def
ls log* -l | wc -l

这里有很多答案,但有些没有考虑在内

  • 包含空格、换行符或控制字符的文件名
  • 以连字符开头的文件名(想象一个名为-l的文件)
  • 隐藏文件,以点开始(如果glob是*.log而不是log*
  • 匹配glob的目录(例如,一个名为logs的目录匹配log*)
  • 空目录(即结果为0)
  • 非常大的目录(列出所有目录会耗尽内存)

这里有一个解决方案可以处理所有这些问题:

ls 2>/dev/null -Ubad1 -- log* | wc -l

解释:

  • -U导致ls不对条目排序,这意味着它不需要在内存中加载整个目录列表
  • -b为非图形字符打印c风格转义,关键是导致换行符打印为\n
  • -a打印出所有文件,甚至隐藏文件(当glob log*暗示没有隐藏文件时不严格需要)
  • -d打印出目录,而不试图列出目录的内容,这是ls通常会做的
  • -1确保它在一列上(ls在写入管道时自动执行此操作,因此不是严格必要的)
  • 2>/dev/null重定向stderr,因此如果有0个日志文件,则忽略错误消息。(注意,shopt -s nullglob会导致ls列出整个工作目录。)
  • wc -l在目录列表生成时消耗目录列表,因此ls的输出在任何时候都不会在内存中。
  • --文件名使用--与命令分开,这样就不会被理解为ls的参数(如果log*被删除)

shell log*扩展到完整的文件列表,如果文件很多,可能会耗尽内存,所以通过grep运行它会更好:

ls -Uba1 | grep ^log | wc -l

最后一种方法在不使用大量内存的情况下处理超大文件目录(尽管它使用了子shell)。-d不再需要,因为它只列出当前目录的内容。

这是我的一句话。

 file_count=$( shopt -s nullglob ; set -- $directory_to_search_inside/* ; echo $#)

您可以使用-R选项来查找递归目录中的文件

ls -R | wc -l // to find all the files


ls -R | grep log | wc -l // to find the files which contains the word log

您可以在grep上使用模式

这是我经常做的事:

ls log* | awk 'END{打印NR}'

您可以使用shell函数轻松地定义这样的命令。此方法不需要任何外部程序,也不生成任何子进程。它不会尝试危险的ls解析,并很好地处理“特殊”字符(空格、换行符、反斜杠等)。它只依赖于shell提供的文件名扩展机制。它至少兼容sh, bash和zsh。

下面这行定义了一个名为count的函数,该函数打印了调用它时使用的参数的数量。

count() { echo $#; }

只需用所需的模式调用它:

count log*

为了在通配符模式不匹配时使结果正确,必须在展开发生时设置shell选项nullglob(或failglob -这是zsh上的默认行为)。可以这样设置:

shopt -s nullglob    # for sh / bash
setopt nullglob      # for zsh

根据你想要计数的内容,你可能还对shell选项dotglob感兴趣。

不幸的是,至少在bash中,不容易在本地设置这些选项。如果你不想全局地设置它们,最直接的解决方案是以这种更复杂的方式使用函数:

( shopt -s nullglob ; shopt -u failglob ; count log* )

如果你想恢复轻量级语法count log*,或者如果你真的想避免衍生子shell,你可以按照以下方式进行hack:

# sh / bash:
# the alias is expanded before the globbing pattern, so we
# can set required options before the globbing gets expanded,
# and restore them afterwards.
count() {
eval "$_count_saved_shopts"
unset _count_saved_shopts
echo $#
}
alias count='
_count_saved_shopts="$(shopt -p nullglob failglob)"
shopt -s nullglob
shopt -u failglob
count'

作为奖励,这个函数有更广泛的用途。例如:

count a* b*          # count files which match either a* or b*
count $(jobs -ps)    # count stopped jobs (sh / bash)

通过将函数转换为可从PATH调用的脚本文件(或等效的C程序),它也可以由findxargs等程序组成:

find "$FIND_OPTIONS" -exec count {} \+    # count results of a search

我已经对这个答案进行了很多思考,特别是考虑到没有办法;t-parse-ls东西。起初,我尝试过

<WARNING! DID NOT WORK>
du --inodes --files0-from=<(find . -maxdepth 1 -type f -print0) | awk '{sum+=int($1)}END{print sum}'
</WARNING! DID NOT WORK>

which worked if there was only a filename like

touch $'w\nlf.aa'

但如果我创建这样的文件名就失败了

touch $'firstline\n3 and some other\n1\n2\texciting\n86stuff.jpg'

我终于想到了下面这些。注意,我试图获得目录中所有文件的计数(不包括任何子目录)。我认为它,连同@Mat和@Dan_Yard的答案,以及至少有@mogsie设定的大部分要求(我不确定内存)。我认为@mogsie的答案是正确的,但我总是试图远离解析ls,除非它是一个非常具体的情况。

awk -F"\0" '{print NF-1}' < <(find . -maxdepth 1 -type f -print0) | awk '{sum+=$1}END{print sum}'

更可读的:

awk -F"\0" '{print NF-1}' < \
<(find . -maxdepth 1 -type f -print0) | \
awk '{sum+=$1}END{print sum}'

这是专门为文件执行查找,用空字符分隔输出(以避免空格和换行问题),然后计算空字符的数量。文件的数量将比空字符的数量少一个,因为在最后会有一个空字符。

要回答OP的问题,有两种情况需要考虑

1)非递归搜索:

awk -F"\0" '{print NF-1}' < \
<(find . -maxdepth 1 -type f -name "log*" -print0) | \
awk '{sum+=$1}END{print sum}'

2)递归搜索。注意,-name参数内部的内容可能需要针对略有不同的行为(隐藏文件等)进行更改。

awk -F"\0" '{print NF-1}' < \
<(find . -type f -name "log*" -print0) | \
awk '{sum+=$1}END{print sum}'

如果有人想评论这些答案与我在这个答案中提到的答案相比如何,请评论。


注意,我在得到这个答案时得到了这个思考过程。

要计算所有内容,只需将ls管道到单词计数行:

ls | wc -l

要使用模式计数,首先将管道连接到grep:

ls | grep log | wc -l

一个重要的评论

(没有足够的声誉来评论)

这是:

ls -1q some_pattern | wc -l

如果碰巧设置了shopt -s nullglob,它会打印所有常规文件的数量,而不仅仅是带有模式的文件(在CentOS-8和Cygwin上测试)。谁知道ls还有什么没有意义的bug ?

这是正确的并且要快得多:

shopt -s nullglob; files=(some_pattern); echo ${#files[@]};

它完成预期的工作。


运行时间不同。
第一个:CentOS上的0.006, Cygwin上的0.083(以防它被小心使用)。
第二个:CentOS上的0.000, Cygwin上的0.003

下面是一个可以在脚本中使用的通用Bash函数。

    # @see https://stackoverflow.com/a/11307382/430062
function countFiles {
shopt -s nullglob
logfiles=($1)
echo ${#logfiles[@]}
}


FILES_COUNT=$(countFiles "$file-*")

这可以用标准POSIX shell语法完成。

下面是一个简单的count_entries函数:

#!/usr/bin/env sh


count_entries()
{
# Emulating Bash nullglob
# If argument 1 is not an existing entry
if [ ! -e "$1" ]
# argument is a returned pattern
# then shift it out
then shift
fi
echo $#
}

对于紧凑的定义:

count_entries(){ [ ! -e "$1" ]&&shift;echo $#;}

特色POSIX兼容的文件计数器类型:

#!/usr/bin/env sh


count_files()
# Count the file arguments matching the file operator
# Synopsys:
# count_files operator FILE [...]
# Arguments:
# $1: The file operator
#   Allowed values:
#   -a FILE    True if file exists.
#   -b FILE    True if file is block special.
#   -c FILE    True if file is character special.
#   -d FILE    True if file is a directory.
#   -e FILE    True if file exists.
#   -f FILE    True if file exists and is a regular file.
#   -g FILE    True if file is set-group-id.
#   -h FILE    True if file is a symbolic link.
#   -L FILE    True if file is a symbolic link.
#   -k FILE    True if file has its `sticky' bit set.
#   -p FILE    True if file is a named pipe.
#   -r FILE    True if file is readable by you.
#   -s FILE    True if file exists and is not empty.
#   -S FILE    True if file is a socket.
#   -t FD      True if FD is opened on a terminal.
#   -u FILE    True if the file is set-user-id.
#   -w FILE    True if the file is writable by you.
#   -x FILE    True if the file is executable by you.
#   -O FILE    True if the file is effectively owned by you.
#   -G FILE    True if the file is effectively owned by your group.
#   -N FILE    True if the file has been modified since it was last read.
# $@: The files arguments
# Output:
#   The number of matching files
# Return:
#   1: Unknown file operator
{
operator=$1
shift
case $operator in
-[abcdefghLkprsStuwxOGN])
for arg; do
# If file is not of required type
if ! test "$operator" "$arg"; then
# Shift it out
shift
fi
done
echo $#
;;
*)
printf 'Invalid file operator: %s\n' "$operator" >&2
return 1
;;
esac
}


count_files "$@"

示例用法:

count_files -f log*.txt
count_files -d datadir*