使用 BASH 脚本计算当前目录及其所有子目录中与 glob 匹配的所有文件

使用 BASH 脚本计算当前目录及其所有子目录中与 glob 匹配的所有文件

我正在尝试计算当前目录及其所有子目录中与特定 glob 匹配的所有文件。例如,查找所有以“.txt”结尾的文件。

(我必须使用for循环来匹配当前目录中的所有文件,并使用另一个for循环来遍历当前目录的所有子目录)

#!/bin/bash
myglob="$1"
if [ $# -eq 1 ]; then
        dir=$1
else
        echo -n Please enter an ending file name:
        read -r  myglob
fi
# echo Directory $dir

numDir=0
numFile=0
for file in ./*; do
#       if [ -d "$file" ]; then
#               echo $file is a FIRST directory
#               let numDir=numDir+1
        if [[ "$file" == *"$myglob" ]]; then
                echo $file is a FIRST file
                let numFile++
        fi
        for file in ./*/*; do
                if [[ "$file" == *"$myglob" ]]; then
                        echo $file is a SECOND file
                        let numFile++
                fi
        done
done
#echo "$dir" contains "$numDir" directories
echo "$dir" contains "$numFile" files

答案1

您似乎误读了作业的问题。

  1. 它说“当前目录”.,即不是 ~或者~/linux2/q3

  2. 它还说“以及所有子目录”。鉴于这似乎是一个介绍性的 shell 脚本课程,他们极不可能希望您在 bash 中编写自己的代码来递归子目录。那是不是初学者的任务。

    它几乎肯定意味着“使用find,递归子目录的标准工具”。

  3. 它说使用 glob,而不是实现您自己的文件名模式匹配。无论你自己的模式匹配代码写得多好,它都是不是使用全局。

    find有一个-name使用 glob 来匹配文件的选项。

    请注意,它也没有说“匹配文件结尾”或文件扩展名。它说“匹配特定的全局”并给出“.txt”作为例子。一团匹配文件扩展名,但它还可以用于匹配更多内容。

  4. “编写一个 shell 脚本来执行 X”(或类似的词)并不一定意味着“编写一个不使用任何外部程序、仅使用内置命令的 shell 脚本”。事实上,这当然不意味着除非这是明确指出的。

    调用外部程序来完成工作是 shell 脚本所做的事情,这对于 shell 脚本来说是完全正常的和预期的...特别是在使用任何标准 unix 实用程序时,例如findwc

    wc是一个标准程序,可用于计算文件或标准输入中的字符数、行数和/或单词数。在这种情况下,您只想计算行数,因此使用 wc's-l选项。

#!/bin/bash

# Count the number of files matching a glob in the current directory
# and all subdirectories.
#
# The glob can be specified on the command line, in which case it
# MUST be quoted or escaped to prevent the shell from expanding it.
# e.g. use '*.txt' or \*.txt, not just *.txt.
#
# if the glob is not specified on the command line, the script prompts
# for a glob until one is provided.

myglob="$1"

while [ -z "$myglob" ] ; do
  read -p 'Enter a glob: ' myglob
done

numfiles=$(find . -type f -name "$myglob" | wc -l)
echo $numfiles

如果当前目录中的任何文件名有可能包含换行符(即LF字符)(其中unix 文件名中的有效字符),然后使用NUL作为文件名分隔符而不是LF

numfiles=$(find . -type f -name "$myglob" -print0 |
             awk -v RS='\0' '{count++}; END {print count}')

它不使用wc -l,而是使用awk脚本来计算 NUL 分隔的文件名。

或者,正如 Stéphane Chazelas 在评论中指出的那样,您可以使用findand来做到这一点grep

numfiles=$(find .//. -type f -name "$myglob" | grep -c //)

起始.//.目录参数导致find输出前缀为.//.由于 不可能//出现在文件名中find,因此可以使用 来grep -c //对文件进行计数。 only.//在文件名中出现一次,因此无论文件名中是否有换行符,这都有效。

顺便说一句,这是很好的 shell 编程实践总是考虑文件名中可能出现的换行符和其他有问题的字符(例如空格、制表符、分号、与号等),即使您认为这可能不会成为问题。这就是为什么在使用变量时应该始终用双引号引起来的原因之一。这也是为什么使用 NUL 作为文件名分隔符比仅仅使用 LF 更好、更可靠、更安全的原因。

如果你解释了使用 NUL 作为分隔符而不是换行符的原因,那可能值得加分。


更新

即使您需要使用两个 for 循环而不是find,您仍然不应该进行自己的模式匹配。您的代码没有使用 glob 来匹配文件 - 它使用您自己的自定义模式匹配代码。这不是同一件事,甚至不是很接近。

下面是一个使用两个 for 循环的示例,该循环实际上使用 glob 来计算匹配文件的数量。我在每个循环下添加了注释来解释它们,但在脚本中您只需一个循环一个循环地运行。

当前目录的循环1:

for f in $myglob; do
  [ -f "$f" ] && let numFile++
done

for循环是您极少数情况之一的示例$myglob当你使用它时想要引用,因为你shell 来扩展 glob。

在几乎所有其他情况下,您不希望 shell 在命令行上扩展变量,因此您必须将它们用双引号引起来:"$myglob"而不仅仅是$myglob.另外,虽然与此脚本无关,但即使您希望展开数组变量,您仍然应该用双引号引起来"${array[@]}",因为您希望将数组的每个单独元素视为一个“单词”。

无论如何,这用于[ -f "$f" ]测试“$f”是否存在并且是常规文件,因此它只计算文件,而不计算目录(或其他任何内容,例如符号链接或命名管道,又名 fifos)。这与使用find's选项的作用相同-type f

如果您想计算目录./而不是(或以及)文件的数量,您可以使用:

[ -d "$f" ] && let numDir++

直接子目录的循环 2:

for f in */$myglob ; do
  [ -f "$f" ] && let numFile++
done

这几乎与第一个 for 循环相同,只是它是迭代*/$myglob而不是仅仅迭代$myglob

总而言之,就是这样的:

#!/bin/bash
# comments deleted, same as version using find above.

myglob="$1"

while [ -z "$myglob" ] ; do
  read -p 'Enter a glob: ' myglob
done

for f in $myglob; do
  [ -f "$f" ] && let numFile++
done

for f in */$myglob ; do
  [ -f "$f" ] && let numFile++
done

echo "$(pwd)/ and $(pwd)/*/ combined contain $numFile files matching '$myglob'"

与版本不同find,这些循环只会计算当前目录和紧接其下的目录中的文件。它不会更深入地递归到子子目录等。

据我从阅读你的问题中得知,这可能就是你想要的。

find您可以使用该选项来限制递归深度-maxdepth。例如find . -maxdepth 2 -type f -name "$myglob"

答案2

扩展当前目录中的 并计算匹配的名称数量的方法*.txt

set -- ./*.txt

这会将位置参数( 、 等)设置$1$2与通配模式匹配的名称。如果nullglob在 shell 中设置了 shell 选项bash,则如果没有匹配项,这将是一个空列表,否则该列表将包含未展开的模式本身。如果dotglob在 shell 中设置了 shell 选项bash,则列表也将包含隐藏名称(如果有任何与模式匹配的名称)(*否则不匹配隐藏名称)。

位置参数列表的长度是$#

这意味着以下是一个简短的脚本,用于计算并报告当前目录中bash有多少个(可能是隐藏的)名称匹配。*.txt

#!/bin/bash

shopt -s dotglob nullglob
set -- ./*.txt

printf 'There are %d names matching ./*.txt here\n' "$#"

如果我们启用globstarshell 选项,我们就可以访问**,它向下匹配到子目录。然后,我们可以轻松扩展上面的脚本,以在当前目录及以下目录下递归搜索:

#!/bin/bash

shopt -s dotglob nullglob globstar
set -- ./**/*.txt

printf 'There are %d names matching ./**/*.txt here\n' "$#"

如果您愿意,显然可以将匹配的名称存储在命名数组中:

#!/bin/bash

shopt -s dotglob nullglob globstar
names=( ./**/*.txt )

printf 'There are %d names matching ./**/*.txt here\n' "${#names[@]}"

您想在单列中打印匹配的名称,您可以这样做

printf '%s\n' "$@"

或者,如果您在中使用命名数组bash

printf '%s\n' "${names[@]}"

如果您只需要计算常规文件,那么您显然需要迭代与 glob 匹配的名称:

#!/bin/bash

shopt -s nullglob dotglob globstar

regular_files=()

for pathname in ./**/*.txt; do
    if [ -f "$pathname" ] && [ ! -L "$pathname" ]; then
        regular_files+=( "$pathname" )
    fi
done

printf 'There are %d regular files matching ./**/*.txt\n' "${#regular_files[@]}"

上面使用的测试-L真的如果给定的路径名​​是符号链接,那么这里使用的测试组合可确保我们只计算实际的常规文件,而不计算到常规文件的符号链接。

相关内容