阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

写好 shell 脚本的 13 个技巧

  • 2018-07-29
  • 本文字数:6845 字

    阅读完需:约 22 分钟

有多少次,你运行./script.sh,然后输出一些东西,但却不知道它刚刚都做了些什么。这是一种很糟糕的脚本用户体验。我将在这篇文章中介绍如何写出具有良好开发者体验的 shell 脚本。

产品的最终用户通常不懂技术,所以不管你怎么折腾产品代码都无所谓。但脚本代码不一样,它们是开发人员写给开发人员的。

这样会导致一些问题:

  1. 混乱的脚本——我知道,我们都是工程师,读得懂代码,但即使这样,也请为我们这些对 Shell 脚本不是很熟练的人考虑一下(我们在写代码时也会为你们考虑的)。
  2. 满屏的日志和错误输出——就算我们也是工程师,并不代表我们了解你所做的一切。
  3. 弄得一团糟却没有做好清理工作——是的,我们可以顺着你的脚本手动撤销变更,但你真的会让那些信任你的脚本的人这么做吗?

所以,我们可以通过一些方法来为自己和别人写出更好的 shell 脚本。这里给出的所有示例都可以使用与 POSIX 标准兼容的 shell 运行(#!/bin/sh),因为它是最常用的。嫌文章太长了可以只看以下总结部分:

  1. 提供 --help 标记
  2. 检查所有命令的可用性
  3. 独立于当前工作目录
  4. 如何读取输入:环境变量 vs. 标记
  5. 打印对系统执行的所有操作
  6. 如果有必要,提供 --silent 选项
  7. 重新开启显示
  8. 用动画的方式显示进度
  9. 用颜色编码输出
  10. 出现错误立即退出脚本
  11. 自己执行清理工作
  12. 在退出时使用不同的错误码
  13. 在结束时打印一个新行

有时间的话可以接着往下看具体内容:

提供 --help 标记

安装在系统上的二进制文件通常带有 man 帮助文档,但对于脚本来说就不一定了。因此我们通常需要为脚本提供 -h 或 --help 标记来打印有关如何使用脚本的信息。如果其他工程师需要修改脚本,这也可以作为脚本的内联文档:

复制代码
#!/bin/sh
if [ ${#@} -ne 0 ] && [ "${@#"--help"}" = "" ]; then
printf -- '...help...\n';
exit 0;
fi;

这段脚本先计算参数长度(${#@} -ne 0),只有当参数长度不为零时才会检查 --help 标记。下一个条件会检查参数中是否存在字符串“--help” 。第一个条件是必需的,如果参数长度为零则不需要打印帮助信息。

检查所有命令的可用性

脚本通常会调用其他脚本或二进制文件。在调用可能不存在的命令时,请先检查它们是否可用。可以使用“command -v 二进制文件名称”来执行此操作,看看它的退出代码是否为零。如果命令不可用,可以告诉用户应该如何获得这个二进制文件:

复制代码
#!/bin/sh
_=$(command -v docker);
if [ "$?" != "0" ]; then
printf -- 'You don\'t seem to have Docker installed.\n';
printf -- 'Get it: https://www.docker.com/community-edition\n';
printf -- 'Exiting with code 127...\n';
exit 127;
fi;
# ...

独立于当前工作目录

从不同的目录执行脚本可能会发生错误,这样的脚本没有人会喜欢。要解决这个问题,请使用绝对路径(/path/to/something)和脚本的相对路径(如下所示)。

可以使用 dirname $0 引用脚本的当前路径:

复制代码
#!/bin/sh
CURR_DIR="$(dirname $0);"
printf -- 'moving application to /opt/app.jar';
mv "${CURR_DIR}/application.jar" /opt/app.jar;

如何读取输入:环境变量vs.标记

脚本通过两种方式接受输入:环境变量和选项标记(参数)。根据经验,对于不影响脚本行为的值,可以使用环境变量,而对于可能触发脚本不同流程的值,可以使用脚本参数。

不影响脚本行为的变量可能是访问令牌和 ID 之类的东西:

复制代码
#!/bin/sh
# do this
export AWS_ACCESS_TOKEN='xxxxxxxxxxxx';
./provision-everything
# and not
./provisiong-everything --token 'xxxxxxxxxxx';

影响脚本行为的变量可能是需要运行实例的数量、是异步还是同步运行、是否在后台运行等参数:

复制代码
#!/bin/sh
# do this
./provision-everything --async --instance-count 400
# and not
INSTANCE_COUNT=400 ASYNC=true ./provision-everything

打印对系统执行的所有操作

脚本通常会对系统执行有状态的更改。不过,由于我们不知道用户何时会向发送 SIGINT,也不知道脚本错误何时可能导致脚本意外终止,因此很有必要将正在做的事情打印在终端上,这样用户就可以在不去查看脚本的情况下回溯这些步骤:

复制代码
#!/bin/sh
printf -- 'Downloading required document to ./downloaded... ';
wget -o ./downloaded https://some.site.com/downloaded;
printf -- 'Moving ./downloaded to /opt/downloaded...';
mv ./downloaded /opt/;
printf -- 'Creating symlink to /opt/downloaded...';
ln -s /opt/downloaded /usr/bin/downloaded;

在必要时提供 --silent 选项

有些脚本是为了将其输出传给其他脚本。虽说脚本都应该能够单独运行,不过有时候也有必要只让它们把输出结果传给另一个脚本。可以利用 stty -echo 来实现 --silent 标记:

复制代码
#!/bin/sh
if [ ${#@} -ne 0 ] && [ "${@#"--silent"}" = "" ]; then
stty -echo;
fi;
# ...
# before point of intended output:
stty +echo && printf -- 'intended output\n';
# silence it again till end of script
stty -echo;
# ...
stty +echo;
exit 0;

重新开启显示

在使用 stty -echo 关闭脚本显示之后,如果发生致命错误,脚本将终止,而且不会恢复终端输出,这样对用户来说是没有意义的。可以使用 trap 来捕捉 SIGINT 和其他操作系统级别的信号,然后使用 stty echo 打开终端显示:

复制代码
#!/bin/sh
error_handle() {
stty echo;
}
if [ ${#@} -ne 0 ] && [ "${@#"--silent"}" = "" ]; then
stty -echo;
trap error_handle INT;
trap error_handle TERM;
trap error_handle KILL;
trap error_handle EXIT;
fi;
# ...

用动画的方式显示进度

有些命令需要运行很长时间,并非所有脚本都提供了进度条。在用户等待异步任务完成时,可以通过一些方式告诉他们脚本仍在运行。比如在 while 循环中打印一些信息:

复制代码
#!/bin/sh
printf -- 'Performing asynchronous action..';
./trigger-action;
DONE=0;
while [ $DONE -eq 0 ]; do
./async-checker;
if [ "$?" = "0" ]; then DONE=1; fi;
printf -- '.';
sleep 1;
done;
printf -- ' DONE!\n';

或者可以做一些更好玩的小玩意儿,比如 http://mywiki.wooledge.org/BashFAQ/034

用颜色编码输出

在脚本中调用其他二进制文件或脚本时,对它们的输出进行颜色编码,这样就可以知道哪个输出来自哪个脚本或二进制文件。这样我们就不需要在满屏的黑白输出文本中查找想要的输出结果。

理想情况下,脚本应该输出白色(默认的,前台进程),子进程应该使用灰色(通常不需要,除非出现错误),使用绿色表示成功,红色表示失败,黄色表示警告。

复制代码
#!/bin/sh
printf -- 'doing something... \n';
printf -- '\033[37m someone else's output \033[0m\n';
printf -- '\033[32m SUCCESS: yay \033[0m\n';
printf -- '\033[33m WARNING: hmm \033[0m\n';
printf -- '\033[31m ERROR: fubar \033[0m\n';

可以使用\033[Xm,其中 X 代表颜色代码。有些脚本使用\e 而不是\033,但要注意\e 不适用于所有的 UNIX 系统。



可在.sh 中使用的所有颜色和修饰符 https://misc.flogisoft.com/bash/tip_colors_and_formatting

出现错误立即退出脚本

set -e 表示从当前位置开始,如果出现任何错误都将触发 EXIT。相反,set +e 表示不管出现任何错误继续执行脚本。

如果脚本是有状态的(每个后续步骤都依赖前一个步骤),那么请使用 set -e,在脚本出现错误时立即退出脚本。如果要求所有命令都要执行完(很少会这样),那么就使用 set +e。

复制代码
#!/bin/sh
set +e;
./script-1;
./script-2; # does not depend on ./script-1
./script-3; # does not depend on ./script-2
set -e;
./script-4;
./script-5; # depends on success of ./script-4
# ...

自己执行清理工作

大多数脚本在出现错误时不会执行清理工作,能够做好这方面工作的脚本实属罕见,但这样做其实很有用,还可以省下不少时间。前面已经给出过示例,让 stty 恢复正常,并借助 trap 命令来执行清理工作:

复制代码
#!/bin/sh
handle_exit_code() {
ERROR_CODE="$?";
printf -- "an error occurred. cleaning up now... ";
# ... cleanup code ...
printf -- "DONE.\nExiting with error code ${ERROR_CODE}.\n";
exit ${ERROR_CODE};
}
trap "handle_exit_code" EXIT;
# ... actual script...

在退出时使用不同的错误码

在绝大多数 shell 脚本中,exit 0 表示执行成功,exit 1 表示发生错误。对错误与错误码进行一对一的映射,这样有助于脚本调试。

复制代码
#!/bin/sh
# ...
if [ "$?" != "0" ]; then
printf -- 'X happened. Exiting with status code 1.\n';
exit 1;
fi;
# ...
if [ "$?" != "0" ]; then
printf -- 'Y happened. Exiting with status code 2.\n';
exit 2;
fi;

这样做有另一个额外的好处,就是其他脚本在调用你的脚本时,可以根据错误码来判断发生了什么错误。

在结束时打印一个新行

如果你有在遵循脚本的最佳实践,那么可能会使用 printf 代替 echo(它在不同系统中的行为有所差别)。问题是 printf 在命令结束后不会自动添加一个新行,导致控制台看起来是这样的:



这样一点也不酷,可以通过简单的方式打印一个新行:

复制代码
#!/bin/sh
# ... your awesome script ...
printf -- '\n';
exit 0;

这样就可以得到:



别人会感谢你这么做的。

总结

这篇文章大致总结了一些简单易用的技巧,让 shell 脚本更易于调试和使用。

英文原文 https://codeburst.io/13-tips-tricks-for-writing-shell-scripts-with-awesome-ux-19a525ae05ae

感谢张婵对本文的审校。

2018-07-29 18:238370
用户头像

发布了 731 篇内容, 共 433.6 次阅读, 收获喜欢 1997 次。

关注

评论 1 条评论

发布
暂无评论
发现更多内容

详解GaussDB(DWS)的CPU资源隔离管控能力

华为云开发者联盟

数据库 cpu GaussDB 资源管控

第一次读 “Clean” 系列,并没有觉得这是一本多好的书

博文视点Broadview

昆仑芯科技加入龙蜥社区 ,赋能智慧开源,共筑AI芯生态

OpenAnolis小助手

芯片 龙蜥社区 CLA 昆仑芯科技

国内首个纯数字藏品元宇宙世界“ADAMeta”星城宇宙开启公测

最新动态

软件开发模型有哪些?

源字节1号

软件开发

快速上手vs2019

Loken

音视频 5月月更

OpenYurt v0.7.0 版本解读:无侵入的跨网络域解决方案 Raven

阿里巴巴云原生

阿里云 开源 云原生 边缘计算 openyurt

Docker容器:将带UI的程序直接转为Web应用,so easy

华为云开发者联盟

云计算 后端 Docker容器 Web应用

影视作品制作中不可或缺的技术支撑——云渲染技术

Finovy Cloud

服务器 云渲染 GPU算力

银行应将低代码能力作为在评估厂商综合能力的一个维度

易观分析

低代码 银行

陆奇:“黑客精神”过时了吗?答案是永远不会

图灵教育

nginx 程序员 服务器 计算机

LR.Net低代码开发平台 快速设计权限管理模块

力软低代码开发平台

查询语句写了limit 1,为什么依然很慢?

华为云开发者联盟

后端 开发 计算引擎 limit 1

2022年第1季度中国跨境进口零售电商市场规模为983.1亿元

易观分析

跨境电商

手把手教你打造一套最牛的知识管理系统

小炮

Serverless JOB | 传统任务新变革

Serverless Devs

Serverless SAE

开发技术指南 | 最全 Substrate 与 Polkadot 技术文档、教程、课程

One Block Community

区块链 技术 波卡生态

【LeetCode】单词长度的最大乘积Java题解

Albert

LeetCode 5月月更

架构实战营 第 6 期 模块七课后作业

火钳刘明

#架构实战营 「架构实战营」

墨天轮访谈 | 阿里云捷熙:AnalyticDB,人人可用的数据分析服务

墨天轮

数据库 阿里云 国产数据库

技术分享| 快对讲调度系统设计概要

anyRTC开发者

音视频 语音通话 调度 视频通话 快对讲

phpstorm 配置 Xdebug 调试

CRMEB

低代码构建物联网平台,让物联网项目更简单

AIRIOT

物联网 低代码开发

GitHub 3.1K,业界首个流式语音合成系统开源!

百度开发者中心

AI+工业互联网:百度AI专利讲述“中国智造”

百度开发者中心

深入浅出Nginx实战与架构原理

C++后台开发

nginx 架构师 后端开发 Linux服务器开发 C++后台开发

2022云原生安全发展24个洞见

青藤云安全

网络安全 容器安全 镜像安全

Apache APISIX v2.14.1 探索性版本发布,进军更多领域

API7.ai 技术团队

服务注册与发现 API网关 Apache APISIX APISIX 网关

大数据培训实时数仓实践以及架构的演进

@零度

数仓 大数据开发

解读ICDE'22论文:基于鲁棒和可解释自编码器的无监督时间序列离群点检测算法

华为云开发者联盟

数据挖掘 数据库 时间序列 自动编码器

全自动、可视化开发,成为敏捷转型落地关键词

SoFlu软件机器人

写好shell脚本的13个技巧_语言 & 开发_Joseph Matthias Goh_InfoQ精选文章