幾乎任何人遲早都要寫一個Bash指令碼。幾乎沒有人說“是的,我喜歡寫”。這就是為什麼幾乎每個人在編寫它們時都會注意的不多的原因。
Bash繼承了Shell寶座,幾乎可以在所有Linux(包括Docker映像)上找到。這就是大多數後端執行的環境。因此,如果需要指令碼化伺服器應用程式啟動,CI / CD步驟或整合測試執行的指令碼,那麼Bash可以滿足需求。
為了將幾個命令粘合在一起,將輸出從一個傳遞到另一個,然後僅啟動一些可執行檔案,Bash是最簡單,最原生的解決方案。儘管用其他語言編寫更大,更復雜的指令碼是很有意義的,但不能指望Python,Ruby,fish或你認為最好的其他直譯器隨處可見。在將其新增到產品伺服器,Docker映像或CI環境之前,可能應該三思而後再考慮。
但是Bash遠非完美。語法是一場噩夢。錯誤處理很困難。到處都有地雷。我們必須處理它。
Bash指令碼模板事不宜遲,就在這裡。
#!/usr/bin/env bashset -Eeuo pipefailtrap cleanup SIGINT SIGTERM ERR EXITscript_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)usage() { cat <<EOFUsage: $(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 descriptionEOF 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 heremsg "${RED}Read parameters:${NOFORMAT}"msg "- flag: ${flag}"msg "- param: ${param}"msg "- arguments: ${args[*]-}"
現在讓我們更詳細地研究它。
環境相容set -Eeuo pipefail
為了獲得最佳相容性,它引用/usr/bin/env而不是/bin/bash直接引用。
快速失敗#!/usr/bin/env bashcp important_file ./backups/rm important_file
該set命令更改指令碼執行選項。例如,通常Bash不在乎某些命令是否失敗,返回一個非零的退出狀態程式碼。它只是希望地跳到下一個。現在看這個小指令碼:
#!/usr/bin/env bashcp important_file ./backups/rm important_file
如果backups目錄不存在,將會發生什麼?你會在控制檯中收到一條錯誤訊息,但是在你能夠做出反應之前,第二條命令已經刪除了該檔案。
獲取位置script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
此行盡其所能定義指令碼的location目錄,然後為什麼定義它。
假設指令碼目錄也是一個工作目錄,指令碼通常在相對於指令碼位置的路徑上執行,複製檔案並執行命令。而且,只要從指令碼目錄執行指令碼即可。
但是,假設CI配置執行如下指令碼:
/opt/ci/project/script.sh
那麼指令碼不是在專案目錄中執行,而是在CI工具的某些完全不同的工作目錄中執行。可以透過執行指令碼之前轉到目錄來修復它:
cd /opt/ci/project && ./script.sh
但是在指令碼方面解決這個問題要好得多。因此,如果指令碼從同一目錄中讀取某些檔案或執行另一個程式,請按以下方式呼叫它:
cat "$script_dir/my_file"
同時,指令碼不會更改工作目錄位置。如果指令碼是從其他目錄執行的,並且使用者提供了某個檔案的相對路徑,則仍然可以讀取該檔案。
嘗試清理trap cleanup SIGINT SIGTERM ERR EXITcleanup() { trap - SIGINT SIGTERM ERR EXIT # script cleanup here}
考慮一下該指令碼trap的finally塊。在指令碼結尾處–由錯誤或外部訊號引起的正常–該cleanup()功能將被執行。例如,在這裡您可以嘗試刪除指令碼建立的所有臨時檔案。
只要記住,cleanup()不僅可以在結束時呼叫,還可以讓指令碼完成工作的任何部分。嘗試清除的所有資源不一定都將存在。
顯示有用的幫助usage() { cat <<EOFUsage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]Script description here....EOF exit}
具有usage()相對接近指令碼的頂部,它會在兩個方面採取行動:
向不瞭解所有選項並且不想遍歷整個指令碼來發現它們的幫助,作為修改指令碼時的最小文件(例如,你兩週後甚至根本不記得寫過指令碼)。列印好訊息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()如果不想在文字中使用顏色,請刪除該功能。之所以保留它是因為,不必每次都用Google搜尋顏色程式碼,可以會更頻繁地使用顏色。
其次,這些顏色msg()只能與功能一起使用,而不能與echo命令一起使用。
該msg()函式用於列印不是指令碼輸出的所有內容。這不僅包括錯誤,還包括所有日誌和訊息。
簡而言之:stdout用於輸出,stderr用於訊息傳遞。
這就是為什麼在大多數情況下stdout都不應該使用顏色的原因。
msg()傳送有訊息的訊息被髮送到stderr流中並支援特殊序列。
用法:
msg "This is a ${RED}very important${NOFORMAT} 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}
CLI引數主要有三種類型-標誌,命名引數和位置引數。該parse_params()功能支援所有這些。
唯一的公共引數模式(此處未處理)是連線的多個單字母標誌。為了能夠將兩個標誌傳遞為-ab,而不是-a -b,將需要一些其他程式碼。
該while迴圈是解析引數的手動方式。在所有其他語言中,您應該使用內建解析器或可用的庫之一,但是,這就是Bash。
模板中包含示例標誌(-f)和命名引數(-p)。只需更改或複製它們即可新增其他引數。並且不要忘了更新usage()。
Bash中有兩種解析引數的方法。是getopt和getopts。有贊成和反對使用它們的論點。這些工具不是最好的,因為預設情況下getoptmacOS上的行為完全不同,並且getopts不支援長引數(例如--help)。
使用模板只需將其複製貼上即可,就像網際網路上找到的大多數程式碼一樣。
對於Bash,沒有通用npm install等效項。
複製之後,只需要更改4件事:
usage() 帶有指令碼描述的文字cleanup() 內容引數parse_params()-保留--help和--no-color,但替換示例:-f和-p實際的指令碼邏輯可移植性在MacOS(預設,古老的Bash 3.2)和幾個Docker映像上測試了該模板:Debian,Ubuntu,CentOS,Amazon Linux,Fedora有用。