PS: 如果你平时工作中有用到linux bash,那你大概率肯定会用到shell脚本来方便个人的工作,这个模板非常不错,可以仔细研究借鉴下,会有启发收获,也可以方便日后查看使用
模板github链接:https://gist.github.com/m-radzikowski/53e0b39e9a59a1518990e76c2bff8038
以下摘自https://betterdev.blog/minimal-safe-bash-script-template/
Bash 脚本。几乎任何人迟早都需要写一个。几乎没有人说“我就很喜欢写脚本呀”。这就是为什么几乎每个人在编写它们时都不太注意的原因。
我不会试图让你成为 Bash 专家(因为我也不是),但我会向你展示一个最小的模板,它会让你的脚本更安全。你不用感谢我,未来的自己会感谢你的。
为什么要在 Bash 中编写脚本?
Bash 脚本的最佳摘要最近出现在我的 Twitter 提要上:
“就像骑自行车”的反面是“就像在 bash 中编程”。
这句话的意思是无论你做了多少次,你每次都必须重新学习。
— 杰克沃顿 (@JakeWharton) 2020 年 12 月 2 日
但是 Bash 与另一种广受欢迎的语言有一些共同点。就像 JavaScript 一样,它不会轻易消失。虽然我们可以希望 Bash 不会成为几乎所有东西的主要语言,但你的工作中总是会碰见它的。
Bash继承了 shell 的宝座,几乎可以在每个 Linux 上找到,包括 Docker 映像。这是大多数后端运行的环境。因此,如果您需要编写服务器应用程序启动、CI/CD 步骤或集成测试运行脚本,Bash 就可以满足您的需求。
要将几个命令粘合在一起,将输出从一个传递到另一个,然后启动一些可执行文件,Bash 是最简单和最原生的解决方案。虽然用其他语言编写更大、更复杂的脚本非常有意义,但您不能期望 Python、Ruby、fish 或任何其他您认为最好的解释器随处可用。
然而,Bash 远非完美。语法是一场噩梦。错误处理很困难,一不小心就会遇到坑。我们必须处理它。
Bash 脚本模板
#!/usr/bin/env bash
set -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
usage() {
cat << EOF # remove the space between << and EOF, this is due to web plugin issue
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]
Script description here.
Available options:
-h, --help Print this help and exit
-v, --verbose Print script debug info
-f, --flag Some flag description
-p, --param Some param description
EOF
exit
}
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
# script cleanup here
}
setup_colors() {
if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
else
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
fi
}
msg() {
echo >&2 -e "${1-}"
}
die() {
local msg=$1
local code=${2-1} # default exit status 1
msg "$msg"
exit "$code"
}
parse_params() {
# default values of variables set from params
flag=0
param=''
while :; do
case "${1-}" in
-h | --help) usage ;;
-v | --verbose) set -x ;;
--no-color) NO_COLOR=1 ;;
-f | --flag) flag=1 ;; # example flag
-p | --param) # example named parameter
param="${2-}"
shift
;;
-?*) die "Unknown option: $1" ;;
*) break ;;
esac
shift
done
args=("$@")
# check required params and arguments
[[ -z "${param-}" ]] && die "Missing required parameter: param"
[[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"
return 0
}
parse_params "$@"
setup_colors
# script logic here
msg "${RED}Read parameters:${NOFORMAT}"
msg "- flag: ${flag}"
msg "- param: ${param}"
msg "- arguments: ${args[*]-}"
这个想法是不要让它太长。我不想将 500 行滚动到脚本逻辑。同时,我想要为任何剧本打下坚实的基础。但是 Bash 并没有让这一切变得容易,它缺乏任何形式的依赖管理。
一种解决方案是拥有一个包含所有样板和实用程序功能的单独脚本,并在开始时执行它。缺点是必须始终将第二个文件附加到任何地方,从而失去“简单 Bash 脚本”的想法。所以我决定只在模板中放入我认为是最低限度的内容,以使其尽可能简短。
现在让我们更详细地看一下它。
选择脚本运行bash环境
#!/usr/bin/env bash
脚本传统上以 shebang 开头。为了最好的兼容性,它引用/usr/bin/env
,而不是/bin/bash
直接引用。虽然,如果您阅读链接的 StackOverflow 问题中的评论,即使这样有时也会失败。
快速失败
set -Eeuo pipefail
该set
命令更改脚本执行选项。例如,通常 Bash 不关心某个命令是否失败,返回非零退出状态代码。它只是高兴地跳到下一个。现在考虑这个小脚本:
#!/usr/bin/env bash
cp important_file ./backups/
rm important_file
如果backups
目录不存在,会发生什么?确切地说,您将在控制台中收到一条错误消息,但在您做出反应之前,该文件将已被第二个命令删除。
有关哪些选项确切set -Eeuo pipefail
更改以及它们将如何保护您的详细信息,请参阅我几年前收藏在书签中的文章。
尽管您应该知道有一些反对设置这些选项的论据。
获取位置
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
这一行尽最大努力定义了脚本的位置目录,然后我们就cd
到它了。为什么?
通常我们的脚本在相对于脚本位置的路径上操作,复制文件和执行命令,假设脚本目录也是一个工作目录。只要我们从它的目录执行脚本,它就是。
但是,假设我们的 CI 配置执行如下脚本:
/opt/ci/project/script.sh
那么我们的脚本不在项目目录中运行,而是在我们的 CI 工具的一些完全不同的工作目录中运行。我们可以通过在执行脚本之前转到目录来修复它:
cd /opt/ci/project && ./script.sh
但是在脚本方面解决这个问题要好得多。因此,如果脚本从同一目录读取某个文件或执行另一个程序,请像这样调用它:
cat "$script_dir/my_file"
同时,该脚本不会更改 workdir 位置。如果脚本是从其他目录执行的,并且用户提供了某个文件的相对路径,我们仍然可以读取它。
尝试清理
trap cleanup SIGINT SIGTERM ERR EXIT
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
# script cleanup here
}
想想脚本trap
的finally
块。在脚本结束时——正常,由错误或外部信号引起——cleanup()
函数将被执行。例如,您可以在此处尝试删除脚本创建的所有临时文件。
请记住,cleanup()
不仅可以在最后调用,还可以让脚本完成工作的任何部分。您尝试清理的所有资源不一定都存在。
显示有用的帮助
usage() {
cat << EOF # remove the space between << and EOF, this is due to web plugin issue
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]
Script description here.
...
EOF
exit
}
相对usage()
靠近脚本的顶部,它将以两种方式起作用:
- 为不知道所有选项并且不想浏览整个脚本以发现它们的人显示帮助,
- 当有人修改脚本时作为最小文档(例如你,2 周后,甚至不记得一开始就写过它)。
我不主张在这里记录每个功能。但是,一个简短、漂亮的脚本使用消息是最低要求。
打印好消息
setup_colors() {
if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
else
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
fi
}
msg() {
echo >&2 -e "${1-}"
}
首先,setup_colors()
如果您不想在文本中使用颜色,请删除该功能。我保留它是因为我知道如果我不必每次都用谷歌搜索代码,我会更频繁地使用颜色。
其次,这些颜色msg()
只能与函数一起使用,而不是与echo
命令一起使用。
该msg()
函数旨在用于打印不是脚本输出的所有内容。这包括所有日志和消息,而不仅仅是错误。引用伟大的12 Factor CLI Apps文章:
简而言之:stdout 用于输出,stderr 用于消息传递。
<cite style="box-sizing: border-box; -webkit-tap-highlight-color: transparent; font-size: 0.8em; color: rgb(119, 119, 119); font-style: normal; font-weight: 400;">Jeff Dickey,对构建 CLI 应用程序略知一二</cite>
stdout
这就是为什么在大多数情况下,无论如何都不应该使用颜色。
打印的消息msg()
被发送到stderr
流并支持特殊序列,如颜色。如果stderr
输出不是交互式终端或 传递了标准参数之一,则无论如何都会禁用颜色。
用法:
msg "This is a ${RED}very important${NOFORMAT} message, but not a script output value!"
要检查它在stderr
不是交互式终端时的行为,请在脚本中添加如上所述的一行。然后将其重定向stderr
到stdout
并通过管道传输到cat
. 管道操作使输出不再直接发送到终端,而是发送到下一个命令,因此现在应该禁用颜色。
$ ./test.sh 2>&1 | cat
This is a very important message, but not a script output value!
解析任何参数
parse_params() {
# default values of variables set from params
flag=0
param=''
while :; do
case "${1-}" in
-h | --help) usage ;;
-v | --verbose) set -x ;;
--no-color) NO_COLOR=1 ;;
-f | --flag) flag=1 ;; # example flag
-p | --param) # example named parameter
param="${2-}"
shift
;;
-?*) die "Unknown option: $1" ;;
*) break ;;
esac
shift
done
args=("$@")
# check required params and arguments
[[ -z "${param-}" ]] && die "Missing required parameter: param"
[[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"
return 0
}
如果在脚本中参数化有任何意义,我通常会这样做。即使脚本只在一个地方使用。它使复制和重用它变得更容易,这通常迟早会发生。此外,即使需要硬编码,通常也有比 Bash 脚本更高级别的更好的地方。
CLI 参数主要分为三种类型——标志、命名参数和位置参数。该parse_params()
功能支持所有这些。
这里没有处理的唯一一种常见参数模式是连接多个单字母标志。为了能够将两个标志传递为-ab
,而不是-a -b
,需要一些额外的代码。
while
循环是一种手动解析参数的方式。在所有其他语言中,您应该使用内置解析器或可用库之一,但是,这就是 Bash。
示例标志 ( -f
) 和命名参数 ( -p
) 在模板中。只需更改或复制它们以添加其他参数。并且不要忘记更新usage()
之后。
这里重要的一点,通常在您获取 Bash 参数解析的第一个 google 结果时会丢失,是在 unknown option 上抛出错误。脚本收到未知选项的事实意味着用户希望它做一些脚本无法完成的事情。所以用户期望和脚本行为可能会有很大的不同。最好在坏事发生之前完全阻止执行。
在 Bash 中解析参数有两种选择。是getopt
和getopts
。有支持和反对使用它们的论据。我发现这些工具不是最好的,因为默认情况下getopt
macOS 上的行为完全不同,并且getopts
不支持长参数(如--help
)。
使用模板
只需复制粘贴它,就像您在互联网上找到的大多数代码一样。
嗯,实际上,这是非常诚实的建议。对于 Bash,没有通用的npm install
等价物。
复制后,您只需要更改 4 件事:
-
usage()
带有脚本描述的文本 -
cleanup()
内容 - 中的参数
parse_params()
– 保留--help
and--no-color
,但替换示例中的参数:-f
and-p
- 实际脚本逻辑
可移植性
我在 MacOS(默认为古老的 Bash 3.2)和几个 Docker 镜像上测试了模板:Debian、Ubuntu、CentOS、Amazon Linux、Fedora。有用。
显然,它不适用于缺少 Bash 的环境,例如 Alpine Linux。Alpine,作为一个简约的系统,使用非常轻的灰(Almquist shell)。
你可以问一个问题,如果使用几乎可以在任何地方工作的Bourne shell兼容脚本不是更好。至少在我的情况下,答案是否定的。Bash 更安全、更强大(但仍然不容易使用),所以我可以接受缺乏对一些我很少需要处理的 Linux 发行版的支持。
进一步阅读
在使用 Bash 或任何更好的其他语言创建 CLI 脚本时,有一些通用规则。这些资源将指导您如何使小型脚本和大型 CLI 应用程序可靠:
结束语
我不是第一个也不是最后一个创建 Bash 脚本模板的人。一个不错的选择是 这个项目,虽然对于我的日常需求来说有点太大了。毕竟,我尽量让 Bash 脚本尽可能小(而且很少见)。
编写 Bash 脚本时,请使用支持ShellCheck linter的 IDE ,例如 JetBrains IDE。它会阻止你做一些可能适得其反的事情。
我的 Bash 脚本模板也可用作 GitHub Gist(在MIT 许可下):
网友评论