如何在 Linux 上遍历目录树

Linux 上的目录让您可以将文件分组到不同的、单独的集合中。缺点是从一个目录移动到另一个目录以执行重复性任务变得乏味。这是自动化的方法。
所有关于目录
当您开始接触 Linux 时,您学习的第一个命令可能是 ls
,但 cd
紧随其后。了解目录以及如何在目录中移动,尤其是嵌套的子目录,是了解 Linux 如何组织自身以及如何将自己的工作组织到文件、目录和子目录中的基本部分。
掌握目录树的概念——以及如何在它们之间移动——是你在熟悉 Linux 的过程中通过的许多小里程碑之一。使用带路径的 cd
会将您带到该目录。像 cd ~
或 cd
这样的快捷方式本身可以带你回到你的主目录,而 cd ..
可以让你在目录中上一级树。简单的。
但是,没有一种同样简单的方法可以在目录树的所有目录中运行命令。我们可以通过不同的方式实现该功能,但没有专门用于该目的的标准 Linux 命令。
某些命令,例如 ls
,具有强制它们递归操作的命令行选项,这意味着它们从一个目录开始,并有条不紊地处理该目录下的整个目录树.对于 ls
,它是 -R
(递归)选项。
如果你需要使用不支持递归的命令,你必须自己提供递归功能。以下是如何做到这一点。
树命令
tree
命令不会帮助我们完成手头的任务,但它确实可以很容易地查看目录树的结构。它在终端窗口中绘制树,以便我们可以即时概览构成目录树的目录和子目录,以及它们在树中的相对位置。
你需要安装 tree
。
在 Ubuntu 上你需要输入:
sudo apt install tree

在 Fedora 上,使用:
sudo dnf install tree

在 Manjaro 上,命令是:
sudo pacman -Sy tree

使用不带参数的 tree
绘制出当前目录下的树。
tree

您可以在命令行上将路径传递给 tree
。
tree work

-d
(目录)选项排除文件,只显示目录。
tree -d work

这是获得目录树结构的清晰视图的最便捷方式。此处显示的目录树是以下示例中使用的目录树。有五个文本文件和八个目录。
不要将 ls 的输出解析为遍历目录
您的第一个想法可能是,如果 ls
可以递归地遍历目录树,为什么不使用 ls
来做到这一点,并将输出通过管道传输到其他一些解析目录的命令中执行某些操作?
解析 ls
的输出被认为是不好的做法。由于 Linux 能够创建包含各种奇怪字符的文件名和目录名,因此创建通用的、普遍正确的解析器变得非常困难。
您可能永远不会故意创建像这样荒谬的目录名称,但脚本或应用程序中的错误可能会。

解析合法但考虑不周的文件和目录名称很容易出错。我们可以使用其他方法,它们比依赖于解释 ls
的输出更安全、更可靠。
使用查找命令
find
命令具有内置的递归功能,它还具有为我们运行命令的能力。这使我们能够构建强大的单行代码。如果它是您将来可能想要使用的东西,您可以将单行代码变成别名或 shell 函数。
此命令递归循环遍历目录树,查找目录。每次它找到一个目录时,它都会打印出目录的名称并在该目录中重复搜索。完成搜索一个目录后,它会退出该目录并在其父目录中继续搜索。
find work -type d -execdir echo "In:" {} \;

您可以按目录列出的顺序查看搜索在树中的进展情况。通过将 tree
命令的输出与 find
单行命令的输出进行比较,您将看到 find
如何搜索每个目录和子目录依次直到它到达一个没有子目录的目录。然后它返回上一层并在该层恢复搜索。
以下是命令的构成方式。
- find:
find
命令。 - work:开始搜索的目录。这可以是路径。
- -type d:我们正在寻找目录。
- -execdir:我们将在我们找到的每个目录中执行一条命令。
- echo “In:”{}:这是命令。我们只是将目录名称回显到终端窗口。 “{}”包含当前目录的名称。
- \;:这是一个用于终止命令的分号。我们需要用反斜杠转义它,这样 Bash 就不会直接解释它。
稍作改动,我们就可以让 find 命令返回与搜索线索匹配的文件。我们需要包括 -name 选项和搜索线索。在这个例子中,我们正在寻找匹配“*.txt”的文本文件,并将它们的名称回显到终端窗口。
find work -name "*.txt" -type f -execdir echo "Found:" {} \;

搜索文件还是目录取决于您想要实现的目标。要在每个目录中运行命令,请使用-type d
。要对每个匹配文件 运行命令,请使用-type f
。
此命令计算起始目录和子目录中所有文本文件的行数。
find work -name "*.txt" -type f -execdir wc -l {} \;

使用脚本遍历目录树
如果您需要遍历脚本内的目录,您可以在脚本内使用 find
命令。如果您需要(或只是想)自己进行递归搜索,您也可以这样做。
#!/bin/bash
shopt -s dotglob nullglob
function recursive {
local current_dir dir_or_file
for current_dir in $1; do
echo "Directory command for:" $current_dir
for dir_or_file in "$current_dir"/*; do
if [[ -d $dir_or_file ]]; then
recursive "$dir_or_file"
else
wc $dir_or_file
fi
done
done
}
recursive "$1"
将文本复制到编辑器中并将其保存为“recurse.sh”,然后使用 chmod
命令使其可执行。
chmod +x recurse.sh

该脚本设置了两个 shell 选项,dotglob
和 nullglob
。
dotglob
设置意味着当展开通配符搜索词时,将返回以句点“.
”开头的文件和目录名称。这实际上意味着我们在搜索结果中包含了隐藏文件和目录。
nullglob
设置意味着未找到任何结果的搜索模式将被视为空字符串或空字符串。他们不会默认搜索词本身。换句话说,如果我们使用星号通配符“*
”搜索目录中的所有内容,但没有结果,我们将收到空字符串而不是包含星号的字符串。这可以防止脚本无意中尝试打开名为“*”的目录,或将“*”视为文件名。
接下来,它定义了一个名为 recursive
的函数。这就是有趣的事情发生的地方。
声明了两个变量,称为 current_dir
和 dir_or_file
。这些是局部变量,只能在函数内引用。
函数中还使用了名为 $1
的变量。这是调用函数时传递给函数的第一个(也是唯一一个)参数。
该脚本使用两个 for
循环,一个嵌套在另一个循环中。第一个(外部)for
循环用于两件事。
一种是运行您希望在每个目录中执行的任何命令。我们在这里所做的只是将目录名称回显到终端窗口。您当然可以使用任何命令或命令序列,或调用另一个脚本函数。
外层 for 循环做的第二件事是检查它能找到的所有文件系统对象——文件或目录。这就是内部 for
循环的目的。依次将每个文件或目录名称传递到 dir_or_file
变量中。
然后在 if 语句中测试 dir_or_file
变量,看它是否是一个目录。
- 如果是,该函数将调用自身并将目录名称作为参数传递。
- 如果
dir_or_file
变量不是目录,那么它一定是文件。可以从if
语句的else
子句调用您希望应用于该文件的任何命令。您还可以在同一个脚本中调用另一个函数。
脚本的最后一行调用递归
函数并传入第一个命令行参数$1
作为搜索的起始目录。这是是什么开始了整个过程。
让我们运行脚本。
./recurse.sh work

遍历目录,脚本中将在每个目录中运行命令的点由“Directory command for:”行指示。找到的文件对它们运行 wc
命令以计算行数、单词数和字符数。
处理的第一个目录是“工作”,然后是树的每个嵌套目录分支。
值得注意的一点是,您可以更改目录的处理顺序,方法是将特定于目录的命令从内部 for 循环上方移至其下方。
让我们将“Directory command for:”行移动到内部 for
循环的 done
之后。
#!/bin/bash
shopt -s dotglob nullglob
function recursive {
local current_dir dir_or_file
for current_dir in $1; do
for dir_or_file in "$current_dir"/*; do
if [[ -d $dir_or_file ]]; then
recursive "$dir_or_file"
else
wc $dir_or_file
fi
done
echo "Directory command for:" $current_dir
done
}
recursive "$1"
现在我们将再次运行脚本。
./recurse.sh work

这一次,目录首先从最深层次向它们应用命令,然后向上处理树的分支。作为参数传递给脚本的目录最后处理。
如果首先处理更深的目录很重要,那么您可以这样做。
递归很奇怪
这就像用自己的电话给自己打电话,给自己留言,告诉自己下一次遇见你的时间——重复。
在您掌握它的好处之前可能需要付出一些努力,但是当您这样做时,您会发现它是一种解决难题的优雅编程方式。