日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Bash Cookbook 学习笔记 【高级】

發布時間:2025/4/5 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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 directory

linux和很多其他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
  • 另一種方法:
export PATH=$(getconf 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原生的命令,通過掩碼改變創建文件(包括目錄)時的默認權限。

用戶組其他八進制
原來的默認權限rwxrwxrwx
二進制111111111777
掩碼位001011011133
掩碼后默認權限110100100644
rw-r--r--
# 注意:該設置對命令行已被重定向的文件不會產生影響 # 設置成變量形式,便于根據需要修改 UMASK=002 umask $UMASK

偵測外部可寫目錄 【腳本】

外部可寫(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位八進制的絕對值
$ chmod 0755 some_script

很多人的習慣,是只使用后三位數。第一位是個特殊位,很少用到。但顯式的寫全四位能避免歧義。

  • 符號表示的相對值([ugo]+/-/=[rwx])
$ chmod -x some_script$ chmod ugo+rx some_script

相對值假設你知道原來的權限,帶有主觀性。絕對值不會造成誤判,更保險一些。

修改完之后最好用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} 68981103829905

0700和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 | less

sudo批量命令時,這樣寫是錯的。因為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路徑下,給出了一些輸入驗證的示范代碼。

  • 帶正負號的數字驗證
#examples/functions/isnum2# 整數 isnum2() {case "$1" in'[-+]' | '') return 1;; # 為空,或只有正負號[-+]*[!0-9]*) return 1;; # 有正負號,但不是數字[-+]*) return 0;; # OK*[!0-9]*) return 1;; # 不是數字*) return 0;; # OKesac }# 浮點數 isnum3() {case "$1" in'') return 1;; # 為空*[!0-9.+-]*) return 1;; # 非數字、正負號或小數點*?[-+]*) return 1;; # 符號不是首位*.*.*) return 1;; # 小數點超過一個*) return 0;; # OKesac }
  • ip地址驗證
# examples/functions/isvalidipis_validip() {case "$*" in""|*[!0-9.]*|*[!0-9]) return 1 ;;esaclocal IFS=. # 以.作為分隔符set -- $* # 將參數分隔后映射到位置變量[ $# -eq 4 ] &&[ ${1:-666} -le 255 ] && [ ${2:-666} -le 255 ] &&[ ${3:-666} -le 255 ] && [ ${4:-666} -le 254 ] }

bash 2.0之后,引入了雙括號[[ ]],用以支持更復雜的匹配語法,并從視覺上區別于老式的單括號[ ]。
其中的雙等號==也可寫為=,但建議用前者。

# 啟用**擴展匹配**(extended globbing) shopt -s extglob# 對大小寫不敏感 shopt -s nocasematch# 匹配次數(關鍵字1|關鍵字2) if [[ 文件名 == *.@(jpg|jpeg) ]] then# ...

擴展匹配 【簡表】

類型匹配次數
@( ... )一次
*( ... )零或多次
+( ... )一或多次
?( ... )零或一次
!( ... )不要匹配

如果擴展匹配還是不能滿足要求,就該正則表達式(以下簡稱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 学习笔记 【高级】的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。