乐趣区

关于java:如何写出安全的基本功能完善的Bash脚本

每个人或多或少总会碰到要应用并且本人实现编写一个最根底的 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。管道操作使输入不再间接发送到终端,而是发送到下一个命令,因而色彩会被禁用。

$ ./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/1…)
  • Command line arguments anatomy explained with examples(https://betterdev.blog/comman…)

Closing notes

我不会是第一个也不是最初一个创立 Bash 脚本模板的人。这个我的项目是一个很好的抉择,尽管对我的日常需要来说有点太大了。毕竟,我尽量使 Bash 脚本尽可能小(而且很少应用)。

编写 Bash 脚本时,请应用反对 ShellCheck linter 的 IDE,如 JetBrains IDEs。它会阻止你做一堆事与愿违的事件。

本文首发:http://blog.didispace.com/min…

欢送关注我的公众号:程序猿 DD,取得独家整顿的收费学习资源助力你的 Java 学习之路!另每周赠书不停哦~

退出移动版