每个人或多或少总会碰到要使用并且自己完成编写一个最基础的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 bashset -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXITscript_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
EOFexit
}cleanup() {trap - SIGINT SIGTERM ERR EXIT# script cleanup here
}setup_colors() {if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; thenNOFORMAT='\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'elseNOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''fi
}msg() {echo >&2 -e "${1-}"
}die() {local msg=$1local code=${2-1} # default exit status 1msg "$msg"exit "$code"
}parse_params() {# default values of variables set from paramsflag=0param=''while :; docase "${1-}" in-h | --help) usage ;;-v | --verbose) set -x ;;--no-color) NO_COLOR=1 ;;-f | --flag) flag=1 ;; # example flag-p | --param) # example named parameterparam="${2-}"shift;;-?*) die "Unknown option: $1" ;;*) break ;;esacshiftdoneargs=("$@")# 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 heremsg "${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 EXITcleanup() {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....
EOFexit
}

尽量让usage()函数相对靠近脚本的顶部,有两种作用:

  • 要为不知道所有选项并且不想查看整个脚本来发现这些选项的人显示帮助。

  • 当有人修改脚本时,保存一个最小的文档(因为两周后,你甚至不记得当初是怎么写的)。

我不主张在这里记录每个函数。但是一个简短、漂亮的脚本使用这些消息是必需的。

Print nice messages

setup_colors() {if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; thenNOFORMAT='\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'elseNOFORMAT='' 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。管道操作使输出不再直接发送到终端,而是发送到下一个命令,因此颜色会被禁用。

$ ./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 paramsflag=0param=''while :; docase "${1-}" in-h | --help) usage ;;-v | --verbose) set -x ;;--no-color) NO_COLOR=1 ;;-f | --flag) flag=1 ;; # example flag-p | --param) # example named parameterparam="${2-}"shift;;-?*) die "Unknown option: $1" ;;*) break ;;esacshiftdoneargs=("$@")# 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。它会阻止你做一堆适得其反的事情。

本文翻译自:https://betterdev.blog/minimal-safe-bash-script-template/

巨星陨落!2007年图灵奖得主Edmund Clarke因感染新冠离世...

2020-12-25

微信突然更新,新增了这些功能...

2020-12-25

Eclipse 官宣,干掉 VS Code !

2020-12-25

2020年Spring Cloud最后一个大版本发布!

2020-12-24

支持Dubbo接口文档生成的工具!

2020-12-24

36 张图梳理 Intellij IDEA 常用设置

2020-12-23

扫一扫,关注我

知晓前沿科技,领略技术魅力

DD自研的沪牌代拍业务

深度交流

如何写出安全的、基本功能完善的Bash脚本相关推荐

  1. 安装包UI美化之路-Electron打包出界面美观,功能完善的安装包,这三步就够了

    这篇文章应该说是<安装包UI美化之路-nsNiuniuSkin安装包制作可视化配置向导>的延伸与进一步应用,在可视配置的基础之上,生成供electron-builder打包的脚本! 一直有 ...

  2. Unity 之 实现读取代码写进Word文档功能实现 -- 软著脚本生成工具

    Unity 之 实现读取代码写进Word文档功能 前言 一,实现步骤 1.1 逻辑梳理 1.2 用到工具 二,实现读写文件 2.1 读取目录相关 2.2 读写文件 三,编辑器拓展 3.1 编辑器拓展介 ...

  3. 如何写出简洁明了的开发功能说明书

    当企业上了ERP 或其他信息系统后,随着业务的发展,总会需要新的功能来助力业务发展,这时就会涉及新功能的开发.不论是标准功能的增强定制,还是自开发新功能,都需要编写开发功能说明书. 身为一枚业务顾问, ...

  4. python写出下列程序的功能_10.下列Python程序的运行结果是( )。

    [填空题]3.以下while循环的循环次数是( ). i=0 while i<10: if i<1:continue if i==5:break i+=1 [单选题]2.设有程序段: k= ...

  5. 用集合return多个值_Python拾珍:用这些功能写出更简洁、更可读或更高效的代码

    本章我会带领大家回顾那些遗漏的地方.Python提供了不少并不是完全必需的功能(不用它们也能写出好代码),但有时候,使用这些功能可以写出更简洁.更可读或者更高效的代码,甚至有时候三者兼得. 19.1 ...

  6. 三菱Plc怎么用c语言编程,如何用程序在三菱PLC上写出配方功能

    如何用程序在三菱PLC上写出配方功能 2018年09月26日 09:05:25来源:今日头条作者:永战胜关键词:PLC编程器 有许多机器客户都要求可以出产多种类型的产品,这些产品工艺相同,仅仅相应的数 ...

  7. 一道面试题:写出SQL语句实现下述功能

    一道面试题:写出SQL语句实现下述功能 题目: 根据表结构写出SQL语句实现下述功能 解析 题目一: 模糊查询(难度:★☆☆☆) 题目二: 聚合查询(难度:★★☆☆) 题目三: 多层嵌套子查询(难度: ...

  8. 基于SSM+MYSQL写的javaWeb房屋租赁管理系统,包括系统前端和后台,页面美观,功能完善,非常高端的SSM源码

    基于SSM+MYSQL写的javaWeb房屋租赁管理系统,包括系统前端和后台,页面美观,功能完善,非常高端的毕业设计 课程设计. ​ 基于SSM+MYSQL写的javaWeb房屋租赁管理系统,包括系统 ...

  9. Python拾珍:用这些功能写出更简洁、更可读或更高效的代码

    本章我会带领大家回顾那些遗漏的地方.Python提供了不少并不是完全必需的功能(不用它们也能写出好代码),但有时候,使用这些功能可以写出更简洁.更可读或者更高效的代码,甚至有时候三者兼得. 19.1 ...

最新文章

  1. java中string与byte[]的转换
  2. 【计算机网络复习 数据链路层】3.6.5 PPP、HDLC
  3. TestNG中使用监听
  4. Java面试之Synchronized无法禁止指令重排却能保证有序性
  5. oracle中制作副本,创建表的副本并在创建时为其提供约束
  6. AndroidStudio_安卓原生开发_保存全局数据---Android原生开发工作笔记141
  7. php冒泡排序的用途,浅谈php冒泡排序
  8. win7 apache php mysql 配置64,win7 64位 Apache+php+mysql配置方法
  9. python如何定义类_Python class定义类,Python类的定义(入门必读)
  10. 【渝粤教育】国家开放大学2018年春季 0551-21T素描(二) 参考试题
  11. python logging 模块之TimedRotatingFileHandler 实现每天一个日志文件
  12. Python_多进程
  13. java当前时间长整数值_在Java中获取当前年份的整数值
  14. python反编译class文件_反编译java class文件
  15. 使用macVLAN网络模式的容器连通性和延迟的测试
  16. sap和erp的区别:
  17. hardware用u盘起动_u盘启动dos最简单的的小方法
  18. 058 不定积分计算工具总结
  19. Instruction Set Principles
  20. 牛逼,一份基于SSM框架实现的支付宝支付功能,附完整源代码...

热门文章

  1. C#只允许启动一个WinFrom进程
  2. linux centos ubuntu yum apt-get 强制使用 ipv4 ipv6
  3. burpsuite 实战指南
  4. docker 镜像名 tag 为none 的解决方案
  5. linux sar命令 性能监控
  6. python3 命令行参数
  7. linux shell 显示路径
  8. VC++实现恢复SSDT
  9. Android开发--多媒体应用开发(一)--MediaPlayer的使用介绍
  10. 利用FreeNas创建AFP共享