Über-Skeleton 挑战

作者:Dave Taylor

我收到了来自 Angela Kahealani 的一条有趣的消息,其中包含一个挑战:“这是我想在 Work the Shell 中看到的内容:一个成熟的 shell 脚本模板。它应该符合所有适用于 CLI 程序的标准。它应该处理日志记录、管道输入、参数、陷阱、临时文件、配置文件等等。” 这是一个有趣的想法,它也很符合我最近几篇文章中一直在谈论的内容:编写快速精简的代码与编写万无一失的脚本之间的区别。那么,让我们开始吧!

解析命令行参数

任何有意义的 shell 脚本的第一步是解析启动参数。Bash 中内置了一个函数来执行此操作,但使用起来相当棘手。例如


while getopts "ab:c" opt; do
  case $opt in
    a)  echo "-a was specified"  ;;
    b)  echo "arg given to b is $OPTARG" ;;
    c)  echo "-c was specified"  ;;
    \?) echo "Invalid option: -$OPTARG" >&2 ;;
  esac
done 

这指定您将有三个可能的参数:-a、-b 和 -c,并且 -b 带有参数。使用 getopts,它们可以以任何顺序出现,并且可以在有意义的地方组合。例如,-cab arg 工作正常,arg 被设置为 -b 的可选参数。但是,-abc arg 将不起作用,因为紧跟在 b 之后的内容需要是其可选参数。

使用 getopts 的好处是它为您完成了所有繁重的工作——无需担心在读取可选参数后进行两次移位等等。如果您给它错误的参数,“?” 值将被触发,并输出错误。

许多程序在所有标志都被“吃掉”后继续解析输入,您还需要代码来处理这种情况。这种情况下的关键变量是 OPTIND,它包含 getopts 已处理的位置参数的数量。解决方案如下


shift $((OPTIND-1)) 

现在 $1 是第一个非起始标志选项;$@ 是给定的所有参数的完整集合,减去所有起始标志等等。

日志消息

如果您的实例化运行不多,那么向脚本添加日志实际上非常容易。您可以使用 syslog,但让我们从最基本的开始


if [ $logging ] ; then
  echo $(date): Status Message >> $logfile
fi 

或者,更好的是,这是一个更简洁的“date”格式和进程 ID


echo $(date '+%F %T') $$: Status Message >> $logfile 

在日志文件本身中,您会看到类似这样的内容


2012-08-07 15:07:56 7026: Status Message 

当有很多事情发生时,这些信息将被证明对调试和分析非常有价值。

但是,如果您确实想使用 syslog 并在标准系统日志文件中获取脚本消息怎么办?这可以使用方便的“logger”程序完成,该程序选项出奇地少,您不需要任何选项。

您只需使用以下命令代替上面的 echo 语句


logger "Status Message"

检查 /var/log/system.log,您可以看到已自动添加的内容


Aug  7 15:12:26 term01 taylor[7100]: status message 

实际上,如果您想真正简化,您可以在您的超级脚本的顶部添加类似这样的内容


if [ $logging ] ; then
  logger="/usr/bin/logger"
else
  logger="echo >/dev/null"
fi 

现在,您可能在系统日志中记录信息的每个调用都将是标准的 /usr/bin/logger 消息或 echo >/dev/null 消息,后者导致信息被丢弃而不会被显示或保存。

捕获信号

对于大多数 shell 脚本,快速的 ^C 会终止它们,仅此而已。但是,对于其他脚本,正在进行更复杂的事情,例如,能够删除临时文件而不是在文件系统上留下碎片是很好的。

这种情况下的关键参与者是一个名为 trap 的程序,它接受两个参数:要调用的函数(或函数名称)以及要与该函数关联的信号或信号集。

这是一个有趣的例子


trap '{ echo "You pressed Ctrl-C" ; exit 1; }' INT 
echo "Counting, press Ctrl-C to exit"
for count in 1 2 3 4 5 6 7 8 9 10; do
    echo $count; sleep 5
done 

如果您运行此命令,您会发现脚本将从 1-10 计数,每个数字之间有 5 秒的延迟。在任何时候,按 Ctrl-C,trap 都会被触发;echo 语句被调用,脚本以非零返回码退出 (exit 1)。

有时您希望脚本在某些地方具有陷阱管理,但在其他地方则没有,在这种情况下,您可以随时通过指定空命令序列来禁用它


trap '' INT 

很简单。代码片段可能看起来类似于


trap '{ /bin/rm -f $tempfile $temp2; exit 1 }' SIGINT 

如果您想知道最后一个参数,它是信号名称。

Linux 世界中定义了很多信号,它们都在 signal 手册页中记录。

最有趣的信号是 SIGINT(用于程序中断);SIGQUIT(用于程序退出请求);SIGKILL(著名的“-9”信号,无法捕获或忽略,并强制立即关闭);SIGALRM(可以用作计时器来约束执行时间);以及 SIGTERM(软件生成的终止请求)。

让我们仔细看看 SIGALRM,因为它在您担心脚本的某一部分可能会永远运行的情况下非常有用。

要设置计时器,请像往常一样使用 trap


trap '{ echo ran out of time ; exit 1 }' SIGALRM 

然后在脚本中的其他地方,在实际调用您担心可能花费太长时间的部分之前,添加类似这样的内容


(
  sleep $delay ; kill -s SIGALRM $$
)& 

这将派生一个子 shell,该子 shell 等待指定的秒数,然后将 SIGALRM 信号发送到父进程(这就是 $$ 指定的,回想一下)。

下个月,我将继续这个有趣的项目,展示 SIGALRM 代码的示例,并为脚本添加一些额外的智能,包括根据脚本是从终端(命令行)还是从重定向的文件/管道接收输入来测试和更改其行为的能力。

您还希望它执行其他花哨的技巧吗?通过 https://linuxjournal.cn/contact 给我们发电子邮件。

键盘照片 通过 Shutterstock.com

Dave Taylor 在 UNIX 和 Linux 系统上编写 shell 脚本已经很长时间了。他是 Learning Unix for Mac OS XWicked Cool Shell Scripts 的作者。您可以在 Twitter 上通过 @DaveTaylor 找到他,您可以通过他的技术问答网站联系他:Ask Dave Taylor

加载 Disqus 评论