如何写出安全的、基本功能完善的Bash脚本
wptr33 2025-07-03 20:21 22 浏览
每个人或多或少总会碰到要使用并且自己完成编写一个最基础的Bash脚本的情况。真实情况是,没有人会说“哇哦,我喜欢写这些脚本”。所以这也是为什么很少有人在写的时候专注在这些脚本上。
我本身也不是一个Bash脚本专家,但是我会在本文中跟你展示一个最基础最简单的安全脚本模板,会让你写的Bash脚本更加安全实用,你掌握了之后肯定会受益匪浅。
为什么要写Bash脚本
其实关于Bash脚本最好的解释如下:
The opposite of "it's like riding a bike" is "it's like programming in bash".
A phrase which means that no matter how many times you do something, you will have to re-learn it every single time.
— Jake Wharton (@JakeWharton)
December 2, 2020
意思就是,跟骑自行车相反,无论做了多少次,每次都感觉像重新学一样。
但是Bash脚本语言和其他一些广受欢迎的语言,例如JavaScript一样,他们不会轻易突然消失,虽然Bash脚本语言不太可能成为业界的主流语言,但实际他就在我们周围,无处不在。
Bash就像继承了shell的衣钵一样,在每台linux上都可以看到他的身影,这可是大多数后端程序运行的环境,因此当你需要编写服务器的应用程序启动、CI/CD步骤或集成测试用的脚本,Bash就在那里等着你。
将几个命令粘在一起,将输出从一个传递到另一个,然后只启动一些可执行文件,Bash是众多方案中最简单的一个。虽然用其他语言编写更大、更复杂的脚本更有效果,但你不能指望Python、Ruby、fish或其他任何你认为最好的程序,可以在任何地方编译使用。所以在将其添加到某个prod server、Docker image或CI环境之前,往往会让人三思而后行。
当然啦,Bash还远远不够完美两个字。他的语法对初学者就像一个噩梦。错误处理也很困难。到处都是我们必须处理掉的陷阱。
Bash script template(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
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[*]-}"
Choose Bash
#!/usr/bin/env bash
脚本为了获得最佳兼容性,它引用/usr/bin/env,而不是直接引用/bin/bash。
Fail fast
set -Eeuo pipefail
set命令可以更改脚本执行选项。例如,通常Bash不关心某个命令是否失败,返回非零退出状态代码。它只是快速地跳到下一个。现在考虑一下这个小脚本:
#!/usr/bin/env bash
cp important_file ./backups/
rm important_file
如果备份目录不存在,会发生什么情况?确切地说,你将在控制台中收到一条错误消息,但是在你能够做出反应之前,该文件已经被第二个命令删除。
Get the location
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"
同时,脚本不会更改工作目录的位置。如果脚本是从其他目录执行的,并且用户提供了指向某个文件的相对路径,我们仍然可以读取它。
Try to clean up
trap cleanup SIGINT SIGTERM ERR EXIT
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
# script cleanup here
}
在脚本结束时,将执行cleanup()函数。你可以在这里尝试删除脚本创建的所有临时文件。
请记住,cleanup()不仅可以在最后调用,在任何时候都可以。
Display helpful help
usage() {
cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]
Script description here.
...
EOF
exit
}
尽量让usage()函数相对靠近脚本的顶部,有两种作用:
- 要为不知道所有选项并且不想查看整个脚本来发现这些选项的人显示帮助。
- 当有人修改脚本时,保存一个最小的文档(因为两周后,你甚至不记得当初是怎么写的)。
我不主张在这里记录每个函数。但是一个简短、漂亮的脚本使用这些消息是必需的。
Print nice messages
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的文章说法:
In short: stdout is for output, stderr is for messaging.
— Jeff Dickey, who knows a little about building CLI apps
stdout用于输出,stderr用于消息传递。
这就是为什么在大多数情况下你不应该为stdout使用颜色。
用msg()打印的消息被发送到stderr流并支持特殊的序列,比如颜色。如果stderr输出不是交互式终端,或者传递了一个标准参数,那么颜色将被禁用。用法如下:
msg "This is a ${RED}very important${NOFORMAT} message, but not a script output value!"
要检查stderr是不是交互式终端时的行为,请在脚本中添加类似于上面的一行。然后执行它,将stderr重定向到stdout并通过管道将其发送到cat。管道操作使输出不再直接发送到终端,而是发送到下一个命令,因此颜色会被禁用。
nbsp;./test.sh 2>&1 | cat
This is a very important message, but not a script output value!
Parse any parameters
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()。
这里最重要的一点是,当您使用第一个google结果进行Bash参数解析时,通常会丢失一个未知选项的错误。脚本收到未知选项的事实意味着用户希望它执行脚本无法完成的操作。所以用户的期望和脚本行为可能会有很大的不同。最好是在坏事发生之前完全阻止处决。
在Bash中解析参数有两种选择。是一个接一个的。有人赞成和反对使用它们。我发现这些工具不是最好的,因为默认情况下,macOS上的getopt行为完全不同,getopts不支持长参数(比如--help)。
Using the template
复制粘贴它,就像你在网上找到的大多数代码一样。
复制后,只需更改4件事:
- 包含脚本说明的usage()文本
- cleanup()内容
- parse_params()中的参数–保留--help和--no color,但替换示例:-f和-p
- 实际的脚本逻辑
Portability
我在MacOS上测试了这个模板(使用默认的bash3.2)和几个Docker映像:Debian、Ubuntu、CentOS、amazonlinux、Fedora。它的确起作用了。
显然,它不能在缺少Bash的环境中工作,比如alpinellinux。
Further reading
在用Bash或其他更好的语言创建CLI脚本时,有一些通用规则。这些资源将指导您如何使小型脚本和大型CLI应用程序可靠,参考如下:
- Command Line Interface Guidelines(https://clig.dev/)
- 12 Factor CLI Apps(https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46)
- Command line arguments anatomy explained with examples(https://betterdev.blog/command-line-arguments-anatomy-explained/)
Closing notes
我不会是第一个也不是最后一个创建Bash脚本模板的人。这个项目是一个很好的选择,虽然对我的日常需求来说有点太大了。毕竟,我尽量使Bash脚本尽可能小(而且很少使用)。
编写Bash脚本时,请使用支持ShellCheck linter的IDE,如JetBrains IDEs。它会阻止你做一堆适得其反的事情。
相关推荐
- MySQL进阶五之自动读写分离mysql-proxy
-
自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...
- 3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?
-
引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...
- 一文由浅入深带你完全掌握MySQL的锁机制原理与应用
-
本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...
- 验证Mysql中联合索引的最左匹配原则
-
后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...
- MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)
-
目录1.索引基础...
- 你会看 MySQL 的执行计划(EXPLAIN)吗?
-
SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...
- MySQL 从入门到精通(四)之索引结构
-
索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...
- mysql总结——面试中最常问到的知识点
-
mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...
- mysql总结——面试中最常问到的知识点(2)
-
首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...
- MySQL基础全知全解!超详细无废话!轻松上手~
-
本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...
- 深入剖析 MySQL 中的锁机制原理_mysql 锁详解
-
在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...
- Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析
-
引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...
- MySQL基础篇:DQL数据查询操作_mysql 查
-
一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...
- MySql:索引的基本使用_mysql索引的使用和原理
-
一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
程序员的开源月刊《HelloGitHub》第 71 期
-
详细介绍一下Redis的Watch机制,可以利用Watch机制来做什么?
-
假如有100W个用户抢一张票,除了负载均衡办法,怎么支持高并发?
-
Java面试必考问题:什么是乐观锁与悲观锁
-
如何将AI助手接入微信(打开ai手机助手)
-
redission YYDS spring boot redission 使用
-
SparkSQL——DataFrame的创建与使用
-
一文带你了解Redis与Memcached? redis与memcached的区别
-
如何利用Redis进行事务处理呢? 如何利用redis进行事务处理呢英文
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)