Bash Cookbook 学习笔记 【高级】
Read Me
- 本文是以英文版<bash cookbook> 為基礎整理的筆記,力求脫水
- 【高級】部分,涉及腳本安全、bash定制、參數設定等高階內容
-
本系列其他兩篇,與之互為參考
- 【基礎】內容涵蓋bash語法等知識點。傳送門
- 【中級】內容包括工具、函數、中斷及時間處理等進階主題。傳送門
-
所有代碼在本機測試通過
- Debian GNU/Linux 9.2 (stretch)
- GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)
- 2018.02.03 更新 【七】編寫安全的腳本.輸入驗證
約定格式
# 注釋:前導的$表示命令提示符 # 注釋:無前導的第二+行表示輸出# 例如: $ 命令 參數1 參數2 參數3 # 行內注釋 輸出_行一 輸出_行二$ cmd par1 par1 par2 # in-line comments output_line1 output_line2七、編寫安全的腳本
安全是一個過程,而不是某種成品、對象、或技術,且沒有終點。 -- Bruce Schneier比如:
- 沒有權限提升的隱患
- 不會意外執行rm -rf /這樣的破壞性代碼
- 不會泄露密碼等敏感信息
- 運行中斷時清理現場 (fail gracefully)
- 對用戶的錯誤輸入有容錯和檢查
- 只使用可信賴的外部文件
- 代碼簡潔、可讀性強,文檔完善,功能明確
當然,以上也適用于所有的軟件。
展開細說,先從腳本頭#!開始
shebang !
shebang#!出現在任何腳本的第一行,它告訴內核,該用什么解釋器來處理該文件。
#!/bin/bash同時,內核也會接收解釋器(比如bash)后邊跟的一個唯一參數(如果用戶提供的話)。該參數會被利用來進行解釋器欺騙(Interpreter Spoofing)。
所以,最好在該位置用減號-占位
#!/bin/bash -但這樣的路徑/bin/是硬編碼的,又會產生各OS之間可移植性的問題。
解決方法:通過原生的env命令,自動識別bash的安裝位置
$ env ... SHELL=/bin/bash ...所以,這樣寫行了嗎?
#!/usr/bin/env bash -還是不行。文件找不到了。
/usr/bin/env: ‘bash -’: No such file or directorylinux和很多其他unix的env,不允許后邊跟兩個或以上的參數,這里參數指的是bash和-。BSD和Solaris等極少數除外。
所以,可移植和安全性有點魚和熊掌的意思,需要自己權衡輕重
# 輕安全,重移植 #!/usr/bin/env bash# 重安全,輕移植 #!/bin/bash -最后,再提一個細節。有時候,你會看到有些腳本的#!和/bin/解釋器之間有一個空格。這是為了向前兼容。很老的系統里需要這個空格。現在的話,可寫可不寫了。
#! /bin/bash安全路徑 $PATH getconf
shebang之后,寫所有其他代碼之前,請先設置安全路徑
- 第一種寫法:
顯式聲明一遍PATH變量,并再次注冊到運行環境。反斜杠用于禁用別名擴展功能。
PATH='/usr/local/bin:/bin:/usr/bin' \export PATH- 另一種方法:
getconf用于獲取系統參數設置
$ getconf -a | grep PATH PATH_MAX 4096 _POSIX_PATH_MAX 4096 PATH /bin:/usr/bin CS_PATH /bin:/usr/bin但第二種寫法還存在個問題:$(變量)移植性不如單引號``。
而且,變量聲明和export注冊放在一條語句內也不是通用的寫法
# 移植性不好 export var='foo'# 最好拆開來寫 var='foo'; export var- 第三種方法:
既然注冊$PATH路徑變量的目的是為了查找工具,那么,可不可以直接指定各工具的路徑呢?
這樣寫腳本會很長。所以最好打包進一個函數內,供各腳本調用。
#!/usr/bin/env bash # 工具查找# 復制、移動、刪除,每個系統都一樣 _cp='/bin/cp' _mv='/bin/mv' _rm='/bin/rm'# 分支判斷 case $(/bin/uname) in'Linux')_cut='/bin/cut'_nice='/bin/nice'# [其他工具];;'SunOS')_cut='/usr/bin/cut'_nice='/usr/bin/nice'# [其他工具];;# [其他系統環境] esac當前路徑 ./
為了減少輸入量,有些用戶習慣把當前路徑.或空路徑(尾部:,或中間::),也加進$PATH變量。
# 當前路徑 PATH=.:$PATH PATH=$PATH:.# 空路徑 PATH='/bin:/usr/bin:' PATH='/bin::/usr/bin'從安全的角度,這是很不好的習慣,尤其是對root賬戶。
因為對命令進行路徑搜索是按$PATH各項依次查找的。
當前路徑在搜索鏈的存在會造成一些不可控的結果。
- 設想一種情況:
如果當前路徑放在最前
$ PATH='.:/bin:/usr/bin'; export PATH此時在tmp目錄執行ls時,系統會先嘗試運行/tmp/ls,而這個ls如果意外存在的話,極可能是木馬命令。
$ cd /tmp; pwd /tmp$ ls # 此處中招了- 再假設一種情況:
點號.放在最后
$ PATH='/bin:/usr/bin:.'; export PATH你機子上恰好裝有一款叫midnight commander程序,它的命令恰好是mc。你在移動文件時mv不小心寫成了mc。本該執行的/bin/mv變成了./mc
$ PATH='/bin:/usr/bin:.'; export PATH$ mc file1 file2 # 此處再次中招以上兩個例子有點極端。但所謂安全,不正是預防此類小概率事件嗎?
禁用別名
惡意的別名類似木馬(trojan),可以誘導用戶執行不安全的命令。
看個簡單的例子。
$ alias unalias=echo$ alias builtin=ls$ builtin unalias vi ls: unalias: No such file or directory ls: vi: No such file or directory$ unalias -a -a通過使用別名,原生的builtin和unalias都被其他命令覆蓋了。
刪除所有的別名,可以消除隱患。
\unalias -a敏感信息
哈希表 hash
當前運行環境下,執行過的命令會被添加到哈希表(hash),用于提高再次調用時的訪問速度。
污染(poison)哈希表
# dog指向cat $ hash -p /bin/cat dog$ hash -l builtin hash -p /bin/cat cat builtin hash -p /bin/cat dog builtin hash -p /bin/stty stty builtin hash -p /usr/bin/clear clear-r開關可用于清空哈希表
# 清理命令路徑下的所有哈希值 hash -r核轉儲 core dump
core dump也被譯為內核轉儲或核心轉儲,這里的內核有別于操作系統內核(kernel)
- core : 應用程序在崩潰瞬間的內存等運行環境的快照,用于調試和分析
- kernel : Linux系統最核心的那部分代碼
被轉儲的內存頁面可能含有密碼等信息,最好禁用該功能。
且最好是寫入系統級的配置文件中,如/etc/profile或~/.bashrc
# 禁用腳本和相關進程的內核轉儲功能 可參考`man 1 bash`的相關章節 ulimit -H -c 0 --# -H 硬上限 # -c 0 核轉儲大小限制為0,即禁用明文密碼
首先一點,千萬千萬不要像這樣寫
$ ./某腳本 -u 用戶 -p 密碼 & [1] 13301就算輸入密碼時,不回顯到屏幕,也不行
read -s -p "password: " PASSWD;因為,以參數形式傳遞給腳本的密碼,始終是以明文的形式存在,通過ps進程列表,或以核轉儲的形式一覽無余
$ psPID TTY TIME CMD2348 pts/1 00:00:00 bash9661 pts/1 00:00:00 ps 13301 pts/1 00:00:00 ./某腳本 -u 用戶 -p 密碼 &如果避免不了要使用明文密碼,可以單獨放進其他用戶沒有查看權限的文件中
$ ./某問題腳本 ~.隱藏目錄/密碼文件像這樣間接引用,至少避免明文暴露的問題。
crypt或其他密碼哈希可行嗎?
首先,哈希是不可逆的,你無法還原回原來的明文。也就是無法訪問那些需要該明文密碼的數據庫。如此,你只能取消數據庫的密碼保護,有點得不償失。
哈希給你的,只是一種"安全"的假象。還不如用明文。
對于明文,一種簡單的防護措施,可以是ROT-13的形式,這個在前邊介紹過。或用47個字符的擴展版本,除了大小寫26個字母外,還支持標點。
$ ROT13=$(echo password | tr 'A-Za-z' 'N-ZA-Mn-za-m')$ ROT47=$(echo password | tr '!-~' 'P-~!-O')這種打亂字母順序的方式,有總比沒有好點,至少不會讓你產生"安全"的假象。
比以上更好的,是sudo,或SSH加密會話。后邊再展開來談。
文件權限 rwxrwxrwx
默認掩碼 umask
umask是bash原生的命令,通過掩碼改變創建文件(包括目錄)時的默認權限。
| 原來的默認權限 | rwx | rwx | rwx | |
| 二進制 | 111 | 111 | 111 | 777 |
| 掩碼位 | 001 | 011 | 011 | 133 |
| 掩碼后默認權限 | 110 | 100 | 100 | 644 |
| rw- | r-- | r-- |
偵測外部可寫目錄 【腳本】
外部可寫(world writable)目錄,是任何其他用戶都有可寫權限的目錄。當然,你肯定不希望此類權限出現在根用戶的$PATH中。
最好能有個腳本,能檢查指定路徑下,此類不安全的目錄是否存在。運行效果類似這樣:
$ ./chkpath.sh; echo $? ok drwxrwsr-x root staff /usr/local/bin ok drwxr-xr-x root root /usr/bin ok drwxr-xr-x root root /bin ok drwxrwsr-x root staff /usr/local/games ok drwxr-xr-x root root /usr/games 外部可寫 drwxrwxrwt root root /tmp 符號鏈接, ok drwxr-xr-x root root /var/run 缺失 /不存在的目錄 2 $ #!/usr/bin/env bash# 統計異常目錄個數 exit_code=0# 列舉所有需要檢查的目錄; for dir in ${PATH//:/ } /tmp /var/run /不存在的目錄 ; do# 如果是符號鏈接[ -L "$dir" ] && printf "%b" "符號鏈接, "# 如果不是目錄if [ ! -d "$dir" ]; thenprintf "%b" "缺失\t\t\t\t"(( exit_code++ ))else# 顯示目錄自身 | 取 [權限,用戶,組]三列stat=$(ls -lHd $dir | awk '{print $1, $3, $4}')# 其他用戶可寫if [ "$(echo $stat | grep '^d.......w. ')" ]; thenprintf "%b" "外部可寫\t$stat "(( exit_code++ ))elseprintf "%b" "ok\t\t$stat "fifiprintf "%b" "$dir\n" doneexit $exit_code該腳本的幾個要點簡單說明一下:
- 變量切割
${PATH//:/ }將路徑變量PATH/的冒號:替換為空格,格式${變量/分隔符/替換值}。
用$IFS=':'的形式也能切割變量,但靈活性不如符號替換。
- for循環
for循環用于實現路徑遍歷,它的明顯優點是有很好的擴展性:
你可以添加任意目錄進來
for dir in 目錄1 目錄2 ...; do... done也可以在循環體內進行任意的條件測試
for dir in ...; do[ -L "$dir" ] && ...if [ ! -d "$dir" ]; then...else...if [ ... ]; then...... done- -d開關
ls -d表示只列出目錄自身,不展示其中的內容。
$ echo ${PATH//:/ } | xargs ls -ldH drwxr-xr-x 2 root root 4096 Jan 21 08:22 /bin drwxr-xr-x 2 root root 36864 Jan 21 08:23 /usr/bin drwxr-xr-x 2 root root 4096 Jul 13 2017 /usr/games drwxrwsr-x 2 root staff 4096 Jul 24 2017 /usr/local/bin drwxrwsr-x 2 root staff 4096 Jul 24 2017 /usr/local/games更改權限 chmod
chmod用于修改目錄及文件權限。
首先,權限可以有兩種表現形式:
- 4位八進制的絕對值
很多人的習慣,是只使用后三位數。第一位是個特殊位,很少用到。但顯式的寫全四位能避免歧義。
- 符號表示的相對值([ugo]+/-/=[rwx])
相對值假設你知道原來的權限,帶有主觀性。絕對值不會造成誤判,更保險一些。
修改完之后最好用ls -l再確認一遍。
關于批量修改:
-R遞歸形式是不建議的。它會將子目錄都設為不可執行,這樣,你就無法訪問這些目錄了。因為cd命令是需要可執行權限的
$ chmod -R 0644 some_directory正確的寫法,是對文件和目錄區別對待,以find | xargs的組合方式進行批量修改
$ find some_directory -type f | xargs chmod 0644 # 文件 $ find some_directory -type d | xargs chmod 0755 # 目錄創建新目錄并設置權限,兩個動作可以用一條命令完成,避免分開執行兩條命令時,產生競態(race condition)的隱患。
$ mkdir -m mode new_directory批量修改權限前,你可能需要對整個系統或特定目錄的權限設置先做備份。
備份文件系統的元數據 【腳本】
#!/usr/bin/env bash # 文件名 archive_meta.shprintf "%b" "權限\t用戶\t組\t大小\t修改時間\t文件描述\n" > archive_file find / \( -path /proc -o -path /mnt -o -path /tmp -o -path /var/tmp \ -o -path /var/cache -o -path /var/spool \) -prune \ -o -type d -printf 'd%m\t%u\t%g\t%s\t%t\t%p/\n' \ -o -type l -printf 'l%m\t%u\t%g\t%s\t%t\t%p -> %l\n' \ -o -printf '%m\t%u\t%g\t%s\t%t\t%p\n' >> archive_file其中的(-path /foo -o -path ...) -prune句段用于排除不需要備份的路徑。-printf進行格式化輸出。效果如下:
$ sudo ./archive_meta.sh $ head archive_file 權限 用戶 組 大小 修改時間 文件描述 d755 root root 4096 Tue Oct 31 04:45:47.2825806270 2017 // d555 root root 0 Fri Jan 26 06:35:45.5240001190 2018 /sys/ d755 root root 0 Fri Jan 26 06:35:45.5360001780 2018 /sys/kernel/ ...這個腳本功能比較簡單,只作為說明用。更專業的文件備份和完整性檢查,可參考Tripwire等工具。
特殊權限 setuid setgid
在腳本中設置特殊位setuid (用戶 user)和setgid (組 group),造成的混亂比解決的問題,要多得多。強烈不建議使用。
簡單介紹一下。
- 如何設置:
先分別創建兩個普通的目錄和文件
$ mkdir suid_dir sgid_dir; touch suid_file sgid_file; ls -l total 8 drwxr-xr-x 2 jimhs jimhs 4096 Jan 26 11:58 sgid_dir/ -rw-r--r-- 1 jimhs jimhs 0 Jan 26 11:58 sgid_file drwxr-xr-x 2 jimhs jimhs 4096 Jan 26 11:58 suid_dir/ -rw-r--r-- 1 jimhs jimhs 0 Jan 26 11:58 suid_file四位權限絕對值的第一位數,4和2,就是setuid位和setgid位
$ chmod 4755 suid_dir suid_file $ chmod 2755 sgid_dir sgid_file再次查看,已經設置好了。用戶和組的x都變成了s
$ ls -l total 8 drwxr-sr-x 2 jimhs jimhs 4096 Jan 26 11:58 sgid_dir/ -rwxr-sr-x 1 jimhs jimhs 0 Jan 26 11:58 sgid_file* drwsr-xr-x 2 jimhs jimhs 4096 Jan 26 11:58 suid_dir/ -rwsr-xr-x 1 jimhs jimhs 0 Jan 26 11:58 suid_file*- 測試是否已設置:
[ -u suid_dir ]及[ -g sgid_file ]用于對用戶和組條件測試。
這兩個值會改變創建和從屬關系,導致不可控的權限泄漏。這也是造成混亂的源頭。所以,沒有關注就沒有傷害~
隔離的環境
隨機數 $RANDOM
在腳本運行環境,使用隨機數命名的臨時目錄及文件,可以增加非法訪問的難度。
最簡單的隨機數生成方式,是使用bash的內置變量${RANDOM}。
$ echo ${RANDOM}${RANDOM}${RANDOM} 689811038299050700和0600權限保證了其他用戶沒有訪問權限。
# 隨機臨時目錄 until [ -n "$temp_dir" -a ! -d "$temp_dir" ]; dotemp_dir="/tmp/自定義前綴.${RANDOM}${RANDOM}${RANDOM}" donemkdir -p -m 0700 $temp_dir \|| { echo "FATAL: 無法創建臨時目錄'$temp_dir': $?"; exit 100 }# 隨機臨時文件 temp_file="$temp_dir/自定義前綴.${RANDOM}${RANDOM}${RANDOM}"touch $temp_file && chmod 0600 $temp_file \|| { echo "FATAL: 無法創建臨時文件'$temp_file': $?"; exit 101 }# 退出前記得刪除臨時目錄 cleanup="rm -rf $temp_dir" trap "$cleanup" ABRT EXIT HUP INT QUIT相比起馬上要介紹的其他方法,${RANDOM}雖然只能生成包含數字的隨機數,但腳本寫起來結構簡單,簡單意味著健壯。移植性好。
也可以這樣生成隨機數:
$ echo $( (last; who; free; date; echo $RANDOM) | md5sum | cut -d' ' -f1 ) c0b5676e55987de62432117842247286即,將一組無規律的命令打包,然后將結果進行哈希,再從中取出特定字段來作為隨機數。這樣做有點取巧,只是提供一種思路。
更專業的實現方式,當然是使用mktemp和/dev/urandom,但考慮到不是任何系統都支持,為了保證腳本的健壯性,避免不了各種繁瑣的驗證和錯誤處理。
創建安全的臨時目錄或文件 【腳本】
# 調用方法: # $temp_file=$(MakeTemp <file|dir> [path/to/name-prefix]) # 示例: # $temp_dir=$(MakeTemp dir /tmp/$PROGRAM.foo) # $temp_file=$(MakeTemp file /tmp/$PROGRAM.foo)function MakeTemp {# 首先,確保$TMP變量已設置[ -n "$TMP" ] || TMP='/tmp'local temp_type=''local sanity_check=''# 類型 file或dirlocal type_name=$1# 如果未指定前綴,則使用$TMP + templocal prefix=${2:-$TMP/temp} case $type_name infile )temp_type=''ur_cmd='touch'# 條件測試: 是常規文件、可讀、可寫、只有我有訪問權限sanity_check='test -f $TEMP_NAME -a \-r $TEMP_NAME -a \-w $TEMP_NAME -a \-O $TEMP_NAME';;dir|directory )temp_type='-d'ur_cmd='mkdir -p -m0700'# 條件測試: 是目錄、可讀、可寫、可執行、只有我有訪問權限sanity_check='test -d $TEMP_NAME -a \-r $TEMP_NAME -a \-w $TEMP_NAME -a \-x $TEMP_NAME -a \-O $TEMP_NAME';;* ) Error "\n$PROGRAM:MakeTemp 參數錯誤! file或dir." 1;;esac# 先試下mktempTEMP_NAME=$(mktemp $temp_type ${prefix}.XXXXXXXXX)# 失敗的話,則用urandomif [ -z "$TEMP_NAME" ]; thenTEMP_NAME="${prefix}.$(cat /dev/urandom | od -x | tr -d ' ' | head -1)"$ur_cmd $TEMP_NAMEfi# 看下創建好沒有,沒有的話只能退出了if ! eval $sanity_check; thenError "\a致命錯誤: 無法創建$type_name with '$0:MakeTemp $*'!\n" 2elseecho "$TEMP_NAME"fi} # MakeTemp函數結束受限控制臺 rbash
rbash即功能受限的控制臺(restricted bash),比如不允許cd到其他目錄、不允許改變環境變量等。具體請參考man rbash
使用前,需要做些必要配置:
- 在/etc/passwd為特定用戶綁定rbash,比如訪客賬戶等
- vi、emacs等可以越權訪問到系統根路徑的危險程序,全部禁用
- 安全命令,放入專門的目錄;$PATH唯一綁定到該目錄
硬幣的另一面:一些實用的程序被禁用后,肯定也影響到使用體驗。而且,總會有漏網之魚。所以,rbash也不是絕對安全的,只不過是門上多了一道鎖。
監獄 chroot
沒錯,這個是叫監獄(jail)。
很好理解,就是把那些可疑的壞腳本或程序,用chroot關進監獄,壞腳本就算要搞破壞,影響也是可控的。
類似于構建了一道隱形的圍墻,chroot會把根路徑/綁定到指定的安全目錄(change root)。該目錄的父節點對里邊的程序是不可見的。結合前一節提到的rbash,很多原本視為“危險”的程序,就沒必要再被禁用了。
但有些程序,天生需要被暴露給外邊的網絡,比如各種DNS、HTTP或郵件服務器等。功能越復雜,管理成本也越高。
擴展閱讀,可參考wiki上關于強制訪問控制MAC的介紹。
權限提升 sudo
sudo允許授權用戶臨時獲得root賬戶權限。
使用前請先花點時間學習該命令、授權配置工具visudo及/etc/sudoers文件(man sudoers)。
類似ALL=(ALL) ALL的授權濫用,會架空系統的整套防御機制。
查看用戶授權
$ sudo -l查看sudo的詳細設置
$ sudo sudo -V | lesssudo批量命令時,這樣寫是錯的。因為sudo只能影響到它后邊的第一個參數。
sudo 命令1 && 命令2 || 命令3正確的寫法
$ sudo bash -c '命令1 && 命令2 || 命令3'能用sudo的地方,就不要使用su。
輸入驗證
所謂驗證,就是定義一種模式,然后將用戶輸入與之比較,結果無外乎兩種,要么匹配,要么不匹配。
常用的句法結構,可以是簡單的一條語句
[模式] && 執行復雜點的,可以是龐大的分支結構
case模式1) 執行1 ;;模式2) 執行2 ;;... esac這些在前邊基礎部分的測試/流程控制都已經都介紹過了。
本節著重講如何定義驗證模式,及如何拆解用戶提供的選項和參數。并結合一些實例,來強化學習。
最簡單的匹配語法,是像這樣:
[ 文件名 == *.jpg ] && echo "是jpg文件"# 模式不要用括號包裹。否則會被理解為字符本身 [ 文件名 == "*.jpg" ] && echo "是jpg文件"在這里,星號*還是作為通配符使用,不要與正則表達式搞混了。
簡單匹配 【簡表】
| * | 任意字符串,包括null |
| ? | 任意單字符 |
| [ ... ] | 匹配括號內的任意字符 |
| [ !... ] | 不匹配括號內的任意字符 |
| [ ^... ] | 不匹配括號內的任意字符 |
簡單匹配 【腳本】
bash安裝包的examples路徑下,給出了一些輸入驗證的示范代碼。
- 帶正負號的數字驗證
- ip地址驗證
bash 2.0之后,引入了雙括號[[ ]],用以支持更復雜的匹配語法,并從視覺上區別于老式的單括號[ ]。
其中的雙等號==也可寫為=,但建議用前者。
擴展匹配 【簡表】
| @( ... ) | 一次 |
| *( ... ) | 零或多次 |
| +( ... ) | 一或多次 |
| ?( ... ) | 零或一次 |
| !( ... ) | 不要匹配 |
如果擴展匹配還是不能滿足要求,就該正則表達式(以下簡稱regex)出場了。
在中級部分講grep工具時,已經介紹過一些常用語法。
正則表達式 【簡表】
其他工具,比如gawk、sed、或是vim等編輯器內,都支持regex語法,但對于bash自身而言,唯一一處會用到regex的地方,就是在[[ ]]這樣的測試語句中。此時,雙等號==要改為=~,以區別于簡單和擴展匹配的語法。
[[ 文件名 =~ [[:alpha:]]{3,6}\.jpg ]] && echo "是jpg文件"方括號內的方括號,是POSIX字符集合,常用的包括:
[[: alnum :]] [[: graph :]] [[: word :]] [[: alpha :]] [[: ascii :]] [[: blank :]] [[: cntrl :]]
[[: digit :]] [[: lower :]] [[: print :]] [[: punct :]] [[: space :]] [[: upper :]] [[: xdigit :]]
復雜一點的例子。比如想用數字編號重命名CD曲目
$ ls Ludwig Van Beethoven - 01 - Allegro.ogg Ludwig Van Beethoven - 02 - Adagio un poco mosso.ogg Ludwig Van Beethoven - 03 - Rondo - Allegro.ogg Ludwig Van Beethoven - 04 - "Coriolan" Overture, Op. 62.ogg Ludwig Van Beethoven - 05 - "Leonore" Overture, No. 2 Op. 72.ogg $文件名的結構:
- 帶空格的字母集- 數字集 - 所有剩下的部分(曲目名稱.后綴)
進一步抽象:
- (regex1)- (regex2) - (regex3)
所以,最終的regex表達式:
- ([[:alpha:][:blank:]]*)- ([[:digit:]]*) - (.*)$
三個圓括號包裹的子表達式,被映射到內置變量BASH_REMATCH數組中,數組第0項表示整條regex語句,其他分別按1、2、3等一一對應。它也是一個內置變量。
for CDTRACK in * doif [[ "$CDTRACK" =~ "([[:alpha:][:blank:]]*)- ([[:digit:]]*) - (.*)$" ]]thenecho Track ${BASH_REMATCH[2]} is ${BASH_REMATCH[3]}mv "$CDTRACK" "Track${BASH_REMATCH[2]}"fi done選項與參數 getops $OPTIND $OPTARG
選項(option)有兩種。
一種不帶參數(argument),類似于一個開關,通過打開或關閉,來改變腳本的行為
# 分開 $ ls -a -l -h ... # 合并 $ ls -alh ...另一種要帶參數
$ mysql -u 用戶名除此之外的,都被視為非選項參數。
以上介紹了四個概念,用個完整的例子來演示:
myscript -a -b alt plow harvest reap其中:
- 開關選項 -a
- 帶參選項 -b
- 選項參數 alt
- 非選項參數 plow harvest reap
在腳本中,如何接收和驗證這些選項和參數?
先貼答案:
#!/usr/bin/env bash #getopts.shaflag= bflag= while getopts 'ab:' OPTION docase $OPTION ina)aflag=1;;b)bflag=1bval="$OPTARG";;?)printf "用法: %s: [-a] [-b value] args\n" $(basename $0) >&2exit 2;;esac doneshift $(($OPTIND – 1))if [ "$aflag" ] thenprintf "選項 -a 已提供\n" fi if [ "$bflag" ] thenprintf '選項 -b "%s" 已提供\n' "$bval" fi printf "剩下的參數是: %s\n" "$*"腳本的核心部分是:
getopts 'ab:' OPTION內置命令getopts,用于接收以減號-開頭的選項。每接收到一個,就放入OPTION變量中,用于后續處理,并返回TRUE。這樣,wihle循環到下一圈。如此反復,直至所有選項被耗盡(取完),或遇到兩個減號--,這時,返回FALSE。while循環終止。
getopts可接受的選項范圍在單引號中定義,這里是a和b。冒號:表示b是帶參選項。如果a是帶參選項,則寫為'a:b'。選項參數會被放入內置變量$OPTARG中。
while循環之后的下一條語句是
shift $(($OPTIND – 1))$OPTIND內置變量用于存放選項和參數的位置索引,初始值是1。每執行一次getopts,該值遞增并指向下個待處理選項。
所以,以下命令在while循環停止的時候,$OPTIND數值是4,指向"plow"的位置。也即通過shift右移3次(3=4-1)達到。
myscript -a -b alt plow harvest reap 位置參數 1 2 3 4腳本中,$*用于取完所有剩下的非選項參數"plow harvest reap"
printf "剩下的參數是: %s\n" "$*"運行效果:
./getopts.sh -ab alt plow harvest reap 選項 -a 已提供 選項 -b "alt" 已提供 剩下的參數是: plow harvest reap自定義錯誤 【腳本】
對于非法選項,getopts會提供默認的錯誤警告信息。如需關閉,可先設置OPTERR=0。
如需使用自定義的錯誤警告,則在getopts定義選項接收范圍時,在最開頭的位置用冒號:標識。
getopts ':ab:' OPTION增加了自定義錯誤警告的腳本:
#!/usr/bin/env bash #getopts.shaflag= bflag= # printf "OPTIND: %d\n" $OPTIND#OPTERR=0 while getopts :ab: FOUND do# printf "OPTIND: %d\n" $OPTINDcase $FOUND ina)aflag=1;;b)bflag=1bval="$OPTARG";;\:) # 反斜杠\表示取消對冒號轉義,下同printf "%s 選項缺少參數\n" $OPTARGprintf "用法: %s: [-a] [-b value] args\n" $(basename $0)exit 2;;\?)printf "未知選項: -%s\n" $OPTARGprintf "用法: %s: [-a] [-b value] args\n" $(basename $0)exit 2;;esac >&2 doneshift $(($OPTIND - 1))if [ "$aflag" ] thenprintf "選項 -a 已提供\n" fiif [ "$bflag" ] thenprintf '選項 -b "%s" 已提供\n' "$bval" fiprintf "剩下的參數是: %s\n" "$*"與前一個例子不同的幾個地方:
- 前導冒號: 當你輸入的選項缺少參數、或選項未定義時,getopts會分別返回字面的冒號:或問號?。同時,該選項符號被放入$OPTARG變量,這樣,就便于在定義錯誤警告的格式化語句中進行引用了。
- 轉義和不轉義的區別: case分支中,冒號:前的反斜杠可寫可不寫。問號?前要寫(即,不做轉義)。兩者都寫,是為了保持一致,更美觀。而前一個例子的問號前之所以不帶反斜杠,是因為把它放在case語句的最后一條缺省分支中,既表示字面的?(也即getopts的返回值),也表示通配符擴展,用來匹配任意字符。
- 重定向: 本例中,將整個case塊都重定向到標準錯誤(STDERR 2),比前例每條printf語句單獨重定向要更好維護。
運行效果:
./getopts.sh -a -b b 選項缺少參數 用法: getopts.sh: [-a] [-b value] args bash現在的主要維護者Chet Ramey,在bash源代碼目錄下(examples/scripts/shprompt),給出了一個輸入驗證的完整模板。內容太長,這里不貼了。有興趣的讀者可以參考。總結
以上是生活随笔為你收集整理的Bash Cookbook 学习笔记 【高级】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Struts2配置文件【代码库】
- 下一篇: bootstrap-table操作之“删