我正在尝试计算当前目录及其所有子目录中与特定 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
您似乎误读了作业的问题。
它说“当前目录”
.
,即不是~
或者~/linux2/q3
它还说“以及所有子目录”。鉴于这似乎是一个介绍性的 shell 脚本课程,他们极不可能希望您在 bash 中编写自己的代码来递归子目录。那是不是初学者的任务。
它几乎肯定意味着“使用
find
,递归子目录的标准工具”。它说使用 glob,而不是实现您自己的文件名模式匹配。无论你自己的模式匹配代码写得多好,它都是不是使用全局。
find
有一个-name
使用 glob 来匹配文件的选项。请注意,它也没有说“匹配文件结尾”或文件扩展名。它说“匹配特定的全局”并给出“.txt”作为例子。一团能匹配文件扩展名,但它还可以用于匹配更多内容。
“编写一个 shell 脚本来执行 X”(或类似的词)并不一定意味着“编写一个不使用任何外部程序、仅使用内置命令的 shell 脚本”。事实上,这当然不意味着除非这是明确指出的。
调用外部程序来完成工作是 shell 脚本所做的事情,这对于 shell 脚本来说是完全正常的和预期的...特别是在使用任何标准 unix 实用程序时,例如
find
或wc
。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 在评论中指出的那样,您可以使用find
and来做到这一点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' "$#"
如果我们启用globstar
shell 选项,我们就可以访问**
,它向下匹配到子目录。然后,我们可以轻松扩展上面的脚本,以在当前目录及以下目录下递归搜索:
#!/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
是真的如果给定的路径名是符号链接,那么这里使用的测试组合可确保我们只计算实际的常规文件,而不计算到常规文件的符号链接。