Contents

在Linux上的多个目录中执行命令

1. 简介

在本教程中,我们将探索一些 Bash 方法来在多个目录中执行相同的命令。

2. 仔细观察

首先,让我们将目标分解为更小的步骤

  1. 列出当前文件夹的内容
  2. 过滤掉所有不是硬链接子文件夹的东西
  3. 对于每个子文件夹运行我们的命令
  4. 对于每个子文件夹,返回步骤 1

逻辑看起来很简单,但第 2 步实际上非常重要,尤其是如果我们执行的命令是不可逆的。

事实上,如果没有第 2 步,我们最终可能会在一个我们不想接触的文件上运行一个具有潜在危险的命令,或者由于符号链接而在一个意外的位置运行

我们将展示如何根据我们选择的方法来处理这些问题。

3. 准备测试环境

在我们深入解决实际问题之前,让我们准备好我们的环境

# create some folders
for folder in 1 2 3
do
    mkdir folder_$folder
done
# create a sub-directory
mkdir folder_1/sub_folder
# create an empty file
touch my_file
# create a symbolic link pointint to folder_3
ln -s folder_3 my_symbolik_link_to_3

让我们检查一下我们创建了什么:

tree
.
├── folder_1
│   └── sub_folder
├── folder_2
├── folder_3
├── my_file
└── my_symbolik_link_to_3 -> folder_3
5 directories, 1 file

现在一切都为我们的脚本准备好了……

4. 循环

在 Bash 中,可以使用不同的内置程序对循环进行编程。 在本节中,我们将一一探讨它们。

如果在任何时候我们需要查看他们的手册页,**我们必须记住内置指令位于主bash 手册页 **中,因此要访问它们,我们必须在终端中运行man bash并在页面中搜索内置关键字(forwhileuntil)。

为了实现第 2 节中描述的逻辑,我们将使用两个测试条件:

  • -d ,如果考虑的路径是目录,则返回true
  • -h,如果考虑的路径是符号链接,则返回true

我们需要这两个条件,因为-d*不会过滤掉指向文件夹的符号链接。*

4.1. for循环

for 循环非常方便,因为我们可以使用它的范围语法轻松检索当前文件夹内容并遍历每个项目:

function recursive_for_loop { 
    for f in *;  do 
        if [ -d $f  -a ! -h $f ];  
        then  
            cd -- "$f";  
            echo "Doing something in folder `pwd`/$f"; 
            # use recursion to navigate the entire tree
            recursive_for_loop;
            cd ..; 
        fi;  
    done;  
};
recursive_for_loop

上面的代码应用了我们前面提到的两个过滤器。因此,我们的代码不会处理任何文件或符号链接

# Result
Doing something in folder /home/user/workspace/folder_1
Doing something in folder /home/user/workspace/folder_1/sub_folder
Doing something in folder /home/user/workspace/folder_2
Doing something in folder /home/user/workspace/folder_3

我们可以观察到,前面定义的条件已成功过滤掉路径中存在的文件和符号链接。

4.2. while循环_

while情况下,我们不能直接从范围中读取,因此我们必须通过管道传递另一个命令的输出 :

function recursive_for_loop { 
    ls -1| while read f; do
        if [ -d $f  -a ! -h $f ];  
        then  
            cd -- "$f";  
            echo "Doing something in folder `pwd`/$f"; 
            # use recursion to navigate the entire tree
            recursive_for_loop;
            cd ..; 
        fi;  
    done;  
};
recursive_for_loop
# Result
Doing something in folder /home/user/workspace/folder_1
Doing something in folder /home/user/workspace/folder_1/sub_folder
Doing something in folder /home/user/workspace/folder_2
Doing something in folder /home/user/workspace/folder_3

4.3. until循环_

until 构造使用相同的技术来读取文件夹列表,但它需要对循环条件进行否定。

这是由于其不同的逻辑:如果条件为,则while运行循环指令,如果条件为,则直到运行它们:

function recursive_for_loop { 
    ls -1| until ! read f; do
        if [ -d $f  -a ! -h $f ];  
        then  
            cd -- "$f";  
            echo "Doing something in folder `pwd`/$f"; 
            # use recursion to navigate the entire tree
            recursive_for_loop;
            cd ..; 
        fi;  
    done; 
};
recursive_for_loop
# Result
Doing something in folder /home/user/workspace/folder_1
Doing something in folder /home/user/workspace/folder_1/sub_folder
Doing something in folder /home/user/workspace/folder_2
Doing something in folder /home/user/workspace/folder_3

5. find命令

循环的替代方法是find 命令,它的主要目的是在目录层次结构中搜索文件。

我们在 Linux 中查找最近修改过的文件一文中看到如何 使用find来搜索最近修改过的文件。

在本例中,我们将探索两个选项*-exec-execdir*,它们的目的相同,即在每个匹配的文件上执行指定的命令。

尽管它们实现了相同的结果,但* -execdir*选项被认为更安全,因为它将从匹配文件(或在我们的情况下为子目录)所在的目录中运行命令,从而避免一些竞争条件。

即便如此,也存在危险:由于 execdir在进入文件夹后执行命令,如果其中包含与我们的命令同名的可执行文件,find将运行本地命令而不是我们想要的命令。

但是对于我们的简单案例场景,它没有任何区别。

为了证明最后一点,让我们使用*-exec选项运行find* :

find ./* -type d -exec touch {}/test \;
# Result
 tree
.
├── folder_1
│   ├── sub_folder
│   │   └── test
│   └── test
├── folder_2
│   └── test
├── folder_3
│   └── test
├── my_file
└── my_symbolik_link_to_3 -> folder_3

该命令在每个子目录中成功生成了“测试”文件。

在继续下一步之前,让我们删除我们刚刚创建的测试文件。

我们可以使用文章Linux 命令 – 删除旧于 X 的文件 的第 2.4 节中已经介绍的相同脚本:

find . -type f -name test -exec rm -i {} \;
# Result
tree
.
├── folder_1
│   └── sub_folder
├── folder_2
├── folder_3
├── my_file
└── my_symbolik_link_to_3 -> folder_3

现在,让我们尝试使用*-execdir选项find*:

find ./* -type d -execdir touch {}/test \;
# Result
tree
.
├── folder_1
│   ├── sub_folder
│   │   └── test
│   └── test
├── folder_2
│   └── test
├── folder_3
│   └── test
├── my_file
└── my_symbolik_link_to_3 -> folder_3

因此,我们证明了这两个选项对于我们的案例场景具有相同的结果。

比较find和循环,我们可以观察到不需要使用过滤条件:选项-type d*过滤掉任何对我们来说不是目录的东西,并且默认情况下,该命令不遵循符号链接。*

如果我们想运行多个命令,我们只需要多次重复相同的选项:

find ./* -type d -execdir echo Doing something in folder {} \; -execdir echo Done something in {} \;
# Result
Doing something in folder ./folder_1
Done something in ./folder_1
Doing something in folder ./sub_folder
Done something in ./sub_folder
Doing something in folder ./folder_2
Done something in ./folder_2
Doing something in folder ./folder_3
Done something in ./folder_3

6. xargs命令

xargs 命令使用标准输入构建和执行命令行。

然后,我们可以通过管道输出find的输出,并在找到的每个目录上执行我们想要的任何命令。

find ./* -type d | xargs -I {} echo Doing something in folder {}
# Result
Doing something in folder ./folder_1
Doing something in folder ./folder_1/sub_folder
Doing something in folder ./folder_2
Doing something in folder ./folder_3

7. 控制搜索深度

以上所有情况都假设我们需要遍历整个目录树,但是如果我们想限制我们想要开始或到达的深度呢?

为此,  find还提供了两个有用的选项:-mindepth和*-maxdepth*。

要应用它们,我们只需要设置我们感兴趣的深度级别,其中数字零代表我们所在的当前目录。

让我们尝试复制与以前相同的行为:

find ./* -mindepth 0 -maxdepth 1 -type d -exec echo Doing something in folder {}\;
# Result
Doing something in folder ./folder_1
Doing something in folder ./folder_1/sub_folder
Doing something in folder ./folder_2
Doing something in folder ./folder_3

现在让我们更改 maxdepth以仅搜索第一级子文件夹:

find ./* -mindepth 0 -maxdepth 0 -type d -exec echo Doing something in folder {}\;
# Result
Doing something in folder ./folder_1
Doing something in folder ./folder_2
Doing something in folder ./folder_3

我们观察到该命令尚未在 sub_folder上执行。

现在,让我们尝试只在树的第二层执行我们的命令,通过更改mindepth 来代替:

find ./* -mindepth 1 -maxdepth 1 -type d -exec echo Doing something in folder {}\;
# Result
Doing something in folder ./folder_1/sub_folder

正如我们所看到的,这些选项增加了对find搜索的更多控制,但是,作为一个缺点,我们需要准确地知道我们有兴趣处理的树结构。