在Shell脚本中使用Exec命令
1. 概述
当我们创建 Bash 脚本时,我们可能希望将所有echo语句的输出重定向到一个日志文件中,而无需在每个echo语句之后显式指定重定向 运算符和日志文件名。Bash exec 命令是一个强大的内置实用程序,可用于此目的。
在本教程中,我们将了解如何使用exec命令将错误和输出日志添加到 shell 脚本。我们还将探索该命令在 shell 脚本中的其他用途。
2. 基础
每当我们在 Bash shell 中运行任何命令时,都会默认创建一个子 shell,并生成一个新的子进程来执行该命令。但是,当使用exec 时,exec 之后的命令会替换当前的 shell。这意味着没有创建子shell,并且当前进程被这个新命令替换。
让我们做一个实验来更好地理解它:
$ pstree -p
init(1)─┬─init(52)───bash(53)
├─init(78)───bash(79)───pstree(108)
└─{init}(7)
$ echo $$
79
$ exec sleep 300
我们使用echo $$ 和pstree 命令检查了当前 shell的PID ,然后使用exec执行了300 秒的sleep。现在让我们切换到另一个终端来检查进程列表:
$ ps -aef | grep $USER
user1 53 52 0 23:37 tty2 00:00:00 -bash
user1 79 78 0 23:39 tty1 00:00:00 sleep 300
user1 111 53 0 23:40 tty2 00:00:00 ps -aef
user1 112 53 0 23:40 tty2 00:00:00 grep --color=auto
正如我们所见,最初分配给 Bash 的 PID 79 现在分配给了sleep命令。
我们还将观察到,在 300 秒的睡眠结束后, PID为 79的会话(终端)退出,因为 shell 被exec命令替换,并且它的执行已经完成。
3. 使用exec命令替换进程
使用不同的命令覆盖现有进程可能是一个强大的工具。让我们用其他一些例子来探讨这个想法。
3.1. 用户登录配置文件
假设 Bash 不是我们 Linux 机器的默认 shell。有趣的是,使用exec命令,我们可以将内存中的默认 shell 替换为 Bash shell,方法是将其添加到用户的登录配置文件中:
exec bash
在某些情况下,我们希望将特定程序或菜单添加到用户的登录配置文件(.bashrc或*.bash_profile*),在这种情况下,我们可以阻止用户在程序退出后拥有 Bash 提示访问权限,而不管其退出状态:
exec operations_menu.sh
3.2. 脚本内的程序调用
我们可以使用exec在脚本中调用脚本或其他程序来覆盖内存中的现有进程。这节省了创建的进程数量,从而节省了系统资源。**当我们不想在子脚本或程序执行后返回主脚本时,**此实现特别有用:
#! /bin/bash
while true
do
echo "1. Disk Stats "
echo "2. Send Evening Report "
read Input
case "$Input" in
1) exec df -kh ;;
2) exec /home/SendReport.sh ;;
esac
done
在这个简单的用户输入驱动脚本中,我们在不同的菜单选项中执行了df命令和使用exec的脚本。
4. 文件描述符和使用exec命令登录 Shell 脚本
exec命令是一个强大的工具,用于操作文件描述符 (FD)、在脚本中创建输出和错误日志记录,只需极少的更改。在 Linux 中,默认情况下,文件描述符 0 是stdin(标准输入),1 是stdout(标准输出),2 是stderr(标准错误)。
4.1. 在脚本中记录
我们可以动态打开、关闭、复制stdout来实现日志操作。让我们将stdout(FD 1)重定向到日志文件:
#! /bin/bash
script_log="/tmp/log_`date +%F`.log"
exec 1>>$script_log
echo "This will be written into log file rather than terminal.."
echo "This too.."
我们检查了如何将标准输出写入文件,现在让我们检查如何将标准错误写入同一个文件:
#! /bin/bash
script_log="/home/shubh/log_`date +%F`.log"
exec 1>>$script_log
exec 2>&1
datee
echo "Above command is wrong, error will be logged in log file"
date
echo "Output of correct date command will also be logged in log file, including these echo statements"
这里我们将stderr (2)复制到stdout (1),stdout已经改成写入日志文件了。
4.2. 将标准输入更改为从文件中读取
让我们创建一个示例输入文件:
$ cat input_csv
SNo,Quantity,Price,Value
1,2,20,40
2,5,10,50
3,1,70,70
现在让我们运行一个示例来读取我们创建的文件:
#! /bin/bash
exec < input_csv
read row1
echo -n "The contents of first line are: "
echo $row1
echo -n "The contents of second line are: "
read row2
echo $row2
这就是我们得到的:
The contents of first line are: SNo,Quantity,Price,Value
The contents of second line are: 1,2,20,40
4.3. 更改文件描述符并恢复默认值
我们还可以打开和关闭新的文件描述符以读取和写入文件。让我们使用与上一节相同的输入文件来演示这一点:
#! /bin/bash
exec 3< input_csv
read -u 3 row1
echo $row1
exec 3<&-
exec 4>&1
exec > out.txt
echo "Output will be logged in out.txt"
exec 4>&-
在这里,我们首先在FD 3上打开输入文件,从文件中read,并在终端上打印其内容(默认为stdout)。最后,我们使用 &- 关闭 FD 3。当此脚本执行时,输出如下:
SNo,Quantity,Price,Value
此外,该脚本还创建一个输出文件out.txt:
$ cat out.txt
Output will be logged in out.txt
请注意,脚本的输出仅包含第一个echo语句。有趣的是,在第一个echo语句之后,我们将stdout复制到FD 4并将其重定向到另一个文件。后来,在最后一行,我们通过关闭FD 4重新获得了对stdout的访问权。
5. 在干净的环境中运行脚本
我们可以使用*-c*选项重置所有环境变量以进行干净运行:
exec -c printenv
由于*printenv 命令列出了环境变量,因此在此处将其作为exec*命令的参数将打印一个空输出。
6. 使用 Find 命令
exec选项可用于对*find 命令找到的文件执行grep*、cat、mv、cp、rm等操作。让我们使用我们文章 中关于find命令的示例来查找“src”目录中包含“interface”一词的所有*.java*文件:
find src -name "*.java" -type f -exec grep -l interface {} \;
在这里,我们指定了选项*-type f* 以仅查找常规文件。此后,我们使用*-exec选项对find命令返回的文件列表执行grep命令。注意末尾的分号会导致对每个文件执行grep命令,一次一个,因为{}*被当前文件名替换。另请注意,需要使用反斜杠来使分号不被 shell 解释。