如何寫出安全的、基本功能完善的Bash腳本

每一個人或多或少總會碰到要使用而且本身完成編寫一個最基礎的Bash腳本的狀況。真實狀況是,沒有人會說「哇哦,我喜歡寫這些腳本」。因此這也是爲何不多有人在寫的時候專一在這些腳本上。linux

我自己也不是一個Bash腳本專家,可是我會在本文中跟你展現一個最基礎最簡單的安全腳本模板,會讓你寫的Bash腳本更加安全實用,你掌握了以後確定會受益不淺。shell

爲何要寫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.xcode

— Jake Wharton (@JakeWharton)安全

December 2, 2020bash

意思就是,跟騎自行車相反,不管作了多少次,每次都感受像從新學同樣。服務器

可是Bash腳本語言和其餘一些廣受歡迎的語言,例如JavaScript同樣,他們不會輕易忽然消失,雖然Bash腳本語言不太可能成爲業界的主流語言,但實際他就在咱們周圍,無處不在。app

Bash就像繼承了shell的衣鉢同樣,在每臺linux上均可以看到他的身影,這但是大多數後端程序運行的環境,所以當你須要編寫服務器的應用程序啓動、CI/CD步驟或集成測試用的腳本,Bash就在那裏等着你。ide

將幾個命令粘在一塊兒,將輸出從一個傳遞到另外一個,而後只啓動一些可執行文件,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應用程序可靠,參考以下:

Closing notes

我不會是第一個也不是最後一個建立Bash腳本模板的人。這個項目是一個很好的選擇,雖然對個人平常需求來講有點太大了。畢竟,我儘可能使Bash腳本儘量小(並且不多使用)。

編寫Bash腳本時,請使用支持ShellCheck linter的IDE,如JetBrains IDEs。它會阻止你作一堆拔苗助長的事情。

本文首發: http://blog.didispace.com/min...

歡迎關注個人公衆號:程序猿DD,得到獨家整理的免費學習資源助力你的Java學習之路!另每週贈書不停哦~

相關文章
相關標籤/搜索