本篇文章介紹一個解析、以及增删改查鍵值對格式配置檔案的 bash shell 腳本。
該 shell 腳本處理的基本配置格式資訊是:key|value。
在腳本中,把 key 稱為 “鍵名”。把 value 稱為 “鍵值”。
把整個 key|value 稱為 “鍵值對”。
把中間的 | 稱為 “分隔符”。
預設的分隔符是 |。腳本裡面提供了設定函數可以修改分隔符的值,以便自定義。
基于這個配置格式,可以配置下面的一些資訊。
配置目錄路徑簡寫
配置一個目錄路徑簡寫,通過一個、或幾個字元,就可以快速 cd 到很深的目錄底下。
例如,在配置檔案中有下面的資訊:
am|frameworks/base/services/core/java/com/android/server/am/ w|frameworks/base/wifi/java/android/net/wifi/
假設有一個 quickcd.sh 腳本可以解析這個配置資訊。
在執行 quickcd.sh w 指令時,該腳本會基于 w 這個鍵名,擷取到 frameworks/base/wifi/java/android/net/wifi/ 這個鍵值。
然後腳本裡面執行 cd frameworks/base/wifi/java/android/net/wifi/ 指令,進入到對應的目錄下。
這樣就不需要輸入多個字元,非常友善。
後面的文章會介紹在不同目錄之間快速來回 cd 的 quickcd.sh 腳本。
同時,所解析的配置資訊儲存在配置檔案裡面。
如果要新增、删除配置項,修改配置檔案自身即可,不需要改動腳本代碼。
這樣可以實作程式資料和程式代碼的分離,友善複用。
配置指令簡寫
配置一個指令簡寫,通過一個、或幾個字元,就可以執行相應的指令。
例如,在配置檔案中有如下的資訊:
l|adb logcat -b all -v threadtimepng|adb shell "screencap /sdcard/screen.png"
這裡配置了 Android 系統的 adb 指令。
類似的,假設有一個 quickadb.sh 腳本可以解析這個配置資訊。
執行 quickadb.sh l 指令,該腳本實際會執行 adb logcat -b all -v threadtime 指令。
這樣可以減少輸入,快速執行内容比較長的指令。
使用配置檔案儲存指令簡寫,可以動态添加、删除指令,跟腳本代碼獨立開來。
後面的文章會介紹一個通過指令簡寫執行對應指令的 tinyshell.sh 腳本。
使用場景總結
總的來說,這裡介紹的配置檔案是基于鍵值對的形式。
常見的使用場景是,提供比較簡單的鍵名來擷取比較複雜的鍵值,然後使用鍵值來進行一些操作。
但是在實際輸入的時候,隻需要輸入鍵名即可,可以簡化輸入,友善使用。
當然,實際使用并不局限于這些場景。
如果有其他基于鍵值對的需求,可以在對應的場景上使用。
腳本使用方法
這個解析配置檔案的 shell 腳本是一個獨立的腳本,可以在其他腳本裡面通過 source 指令進行調用。
假設腳本檔案名為 parsecfg.sh,調用該腳本的順序步驟說明如下:
- source parsecfg.sh:在調用者的腳本中引入 parsecfg.sh 腳本的代碼,以便後續調用 parsecfg.sh 腳本裡面的函數。這裡需要通過 source 指令來調用,才能共享 parsecfg.sh 腳本裡面的函數、全局變量值。
- (可選的)set_info_ifs separatorset_info_ifs:這是 parsecfg.sh 腳本裡面的函數,用于設定分隔符。所給的第一個參數指定新的分隔符。預設分隔符是 |。如果需要解析的配置檔案用的是其他分隔符,就需要先設定分隔符,再解析配置檔案。如果使用預設的分隔符,可以跳過這個步驟。
- open_config_file filenameopen_config_file:這是 parsecfg.sh 腳本裡面的函數,用于解析配置檔案。所給的第一個參數指定配置檔案名。
- (可選的)handle_config_option -l|-v|-i|-e|-a|-dhandle_config_option:這是 parsecfg.sh 腳本裡面的函數,用于處理選項參數。‘-l’ 選項列印配置檔案本身的内容。‘-v’ 選項以鍵值對的形式列印所有配置項的值。‘-i’ 選項後面要跟着一個參數,查詢該參數值在配置檔案中的具體内容。‘-e’ 選項使用 vim 打開配置檔案,以便手動編輯。‘-a’ 選項後面跟着一個參數,把指定的鍵值對添加到配置檔案末尾。‘-d’ 選項後面跟着一個參數,從配置檔案中删除該參數所在的行。如果沒有需要處理的選項,可以跳過這個步驟。
- 解析配置檔案後,就可以調用 parsecfg.sh 腳本提供的功能函數來進行一些操作。get_key_of_entry 函數從 “key|value” 形式的鍵值對中擷取到 key 這個鍵名。get_value_of_entry 函數從 “key|value” 形式的鍵值對中擷取到 value” 這個鍵值。get_value_by_key 函數在配置檔案中基于所給鍵名擷取到對應的鍵值。search_value_from_file 函數在配置檔案中查找所給的内容,列印出比對的行。delete_key_value 函數從配置檔案中删除所給鍵名對應的行。append_key_value 函數把所給的鍵值對添加到配置檔案的末尾。
parsecfg.sh 腳本代碼
列出 parsecfg.sh 腳本的具體代碼如下所示。
在這個代碼中,幾乎每一行代碼都提供了詳細的注釋,友善閱讀。
這篇文章的後面也會提供一個參考的調用例子,有助了解。
#!/bin/bash# 這個腳本提供函數接口來解析、處理鍵值對格式的配置檔案.# 預設的配置格式為: key|value. 該腳本提供如下功能:# 1.根據所提供的 key 擷取到對應的 value.# 2.檢視配置檔案的内容.# 3.使用 vim 打開配置檔案,以供編輯.# 4.提供函數來插入一個鍵值對到配置檔案中.# 5.提供函數從配置檔案中删除所給 key 對應的鍵值對.# 上面的 | 是鍵名和鍵值之間的分隔符.腳本提供set_info_ifs()函數來設定新的值.# 下面變量儲存傳入的配置檔案名.PARSECFG_filepath=""# 定義配置檔案中鍵名和鍵值的分隔符. 預設分隔符是 '|'.# 可以調用 set_info_ifs() 函數來修改分隔符的值,指定新的分隔符.info_ifs="|"######## 下面函數是目前腳本實作的功能函數 ######### 從傳入的項中提取出鍵名,并把鍵名寫到标準輸出,以供調用者讀取.# 下面echo的内容要用雙引号括起來.雙引号可以避免進行路徑名擴充等.# 當所echo的内容帶有 '*' 時,不加雙引号的話, '*' 可能會進行路徑# 名擴充,導緻輸出結果發生變化. 後面的幾個函數也要參照這個處理.get_key_of_entry(){ local entry="$1" # ${param%%pattern} 表達式删除比對的字尾,傳回前面剩餘的部分. echo "${entry%%${info_ifs}*}"}# 從傳入的項中提取出鍵值,并把鍵值寫到标準輸出,以供調用者讀取.get_value_of_entry(){ local entry="$1" # ${param#pattern} 表達式删除比對的字首,傳回後面剩餘的部分. echo "${entry#*${info_ifs}}"}# 該函數根據傳入的鍵名從 key_values 關聯數組中擷取對應鍵值.# 如果比對,将鍵值寫到标準輸出,調用者可以讀取該标準輸出來擷取鍵值.# 該函數把查詢到的鍵值寫入到标準輸出的鍵值. 如果沒有比對所提供# 鍵名的鍵值,輸出會是空. 調用者需要檢查該函數的輸出是否為空.get_value_by_key(){ # 所給的第一個參數是要查詢的鍵名. local key="$1" # 使用鍵名從鍵值對數組中擷取到鍵值,并輸出該鍵值. echo "${key_values["${key}"]}"}# 根據傳入的鍵名删除配置檔案中對應該鍵名的行.delete_entry_by_key(){ # 所給的第一個參數是要删除的鍵名,會删除對應的鍵值對. local key="$1" # 這裡要在${key}的前面加上^,要求${key}必須在行首. sed -i "/^${key}|/d" "${PARSECFG_filepath}" # 将關聯數組中被删除鍵名對應的鍵值設成空. # key_values["${key}"]="" # 将鍵值設成空,這個鍵名還是存在于數組中.可以用 unset name[subscript] # 指令移除指定下标的數組元素.移除之後,這個數組元素在數組中已經不存在. # 注意用雙引号把整個數組元素括起來. unset 指令後面的參數會進行路徑名 # 擴充.例如提供key_values[s]參數,如果目前目錄下有一個key_valuess檔案, # 那麼key_values[s]會對應 key_valuess,而不是對應數組下标為s的數組元素. # 為了避免這個問題,使用雙引号把整個數組元素括起來,不進行路徑名擴充. unset "key_values[${key}]"}# 根據傳入的鍵名,删除它在配置檔案中對應的行delete_key_value(){ if [ $# -ne 1 ]; then echo "Usage: $FUNCNAME key_name" return 1 fi local key="$1" # 如果所給的鍵名在配置檔案中已經存在,get_value_by_key()函數輸出 # 的内容不為空. 判斷該函數的輸出内容,不為空時才進行删除. local value=$(get_value_by_key "${key}") if test -n "${value}"; then delete_entry_by_key "${key}" else echo "出錯: 找不到路徑簡寫 '${key}' 對應的行" fi}# 該函數先從傳入的鍵值對中解析出鍵名,然後執行get_value_by_key()# 函數來判斷該鍵名是否已經在配置檔案中,如果在,就删除該鍵名對應的行.# 最終,新傳入的鍵值對會被追加到配置檔案的末尾.append_key_value(){ if [ $# -ne 1 ]; then echo "Usage: $FUNCNAME key_value" return 1 fi # 所給的第一個參數是完整的鍵值對. local full_entry="$1" # 從傳入的鍵值對中解析出鍵名 local key_name=$(get_key_of_entry "${full_entry}") # 從配置檔案中擷取該鍵名對應的值.如果能夠擷取到值,表示該鍵名已經存在 # 于配置檔案中,會先删除這個鍵值對,再追加新傳入的鍵值對到配置檔案末尾. local match_value=$(get_value_by_key "${key_name}") if test -n "${match_value}"; then echo "更新 ${key_name}${info_ifs}${match_value} 為: ${full_entry}" delete_entry_by_key "${key_name}" fi # 追加新的鍵值對到配置檔案末尾 echo "${full_entry}" >> "${PARSECFG_filepath}" # 将新項的鍵名和鍵值添加到 key_values 數組中,以便實時反應這個修改. key_values["${key_name}"]="$(get_value_of_entry "${full_entry}")"}# 使用 cat 指令将配置檔案的内容列印到标準輸出上.show_config_file(){ echo "所傳入配置檔案的内容為:" cat "${PARSECFG_filepath}"}# 列印從配置檔案中解析得到的鍵值對.show_key_values(){ local key_name # ${!array[@]} 對應關聯數組的所有鍵. ${array[@]}對應關聯數組的所有值. # 下面先擷取關聯數組的鍵,再通過鍵名來擷取鍵值,并把鍵名和鍵值都列印出來. for key_name in "${!key_values[@]}"; do printf "key='e[32m${key_name}e[0m' " printf "value='e[33m${key_values["${key_name}"]}e[0m'" done}# 使用 vim 打開配置檔案,以供編輯. 注意: 使用vim編輯檔案後,檔案所發生的改動不能# 實時在腳本中反應出來,需要再次執行腳本,重新讀取配置檔案才能擷取到所作的修改.# 為了避免這個問題,在退出編輯後,主動調用open_config_file函數,重新解析配置檔案.edit_config_file(){ vim "${PARSECFG_filepath}" # 調用 open_config_file() 函數解析配置檔案,重新為 key_values 指派. open_config_file "${PARSECFG_filepath}"}# 在配置檔案中查找指定的内容,看該内容是否在配置檔案中.search_value_from_file(){ # 如果查找到比對的内容,grep指令會列印比對的内容輸出,以便檢視. grep "$1" "${PARSECFG_filepath}" if [ $? -ne 0 ]; then echo "配置檔案中不包含所給的 '$1'" return 1 fi}######## 下面函數是初始化時需要調用的函數 ######### 該函數用于設定配置檔案中鍵名和鍵值的分隔符.# 所給的第一個參數會指定新的分隔符,并覆寫之前設定的分隔符.set_info_ifs(){ if [ $# -ne 1 ]; then echo "Usage: $FUNCNAME separator" return 1 fi if [ -n "${PARSECFG_filepath}" ]; then # 如果配置檔案名不為空,說明之前已經解析過配置檔案. # 那麼之前解析檔案沒有使用新的分隔符,報錯傳回.需要 # 調用者修改代碼,先調用目前函數,再調用open_config_file() # 函數,以便使用新指定的分隔符來解析配置檔案的内容. echo "出錯: 設定分隔符要先調用 set_info_ifs,再調用 open_config_file." return 2 fi info_ifs="$1"}# 讀取配置檔案,并将配置檔案的内容儲存到關聯數組中. 每次解析配置檔案# 之前,都要先調用該函數.後續直接通過關聯數組來擷取對應的值,不再多次# 打開檔案. 該函數接收一個參數,指定要解析的配置檔案路徑名.open_config_file(){ if [ $# -ne 1 ]; then echo "Usage: $FUNCNAME config_filename" return 1 fi # 判斷所給的配置檔案是否存在,且是否是文本檔案. if [ ! -f "${1}" ]; then echo "ERROR: the file '${1}' does not exist" return 2 fi # 存在配置檔案,則把檔案路徑名儲存到 PARSECFG_filepath 變量. # 使用 readlink -f 指令擷取檔案的絕對路徑,包括檔案名自身. # 一般來說,所給的檔案名是相對路徑.後續 cd 到其他目錄後,用 # 所給的相對路徑會找不到這個檔案, -l 選項無法檢視檔案内容. PARSECFG_filepath="$(readlink -f $1)" # 定義一個關聯數組,儲存配置檔案中的鍵值對. 要先重置key_values的定義, # 避免通過 source 指令調用該腳本時, key_values 所儲存的值沒有被清空, # 造成混亂. 在函數内使用 declare 聲明變量,預設是局部變量,跟 local # 指令類似. 使用 declare -g 可以在函數内聲明變量為全局變量. unset key_values declare -g -A key_values local key value entryline # 逐行讀取配置檔案,并從每一行中解析出鍵名和鍵值,儲存到關聯數組 # key_values中.後續直接通過鍵名來擷取鍵值.如果鍵名不存在,鍵值為空. while read entryline; do # 由于配置檔案的鍵值中可能帶有空格,下面的${entryline}要用雙引号 # 括起來,避免帶有空格時,本想傳入一個參數,卻被分割成了多個參數. # 例如${entryline}是service list,在不加引号時,get_value_of_entry() # 函數會接收到兩個參數,第一個參數是$1,對應service,第二個參數是$2, # 對應list,而get_value_of_entry()函數隻擷取了第一個參數的值,這樣就 # 會處理出錯.在傳遞變量值給函數時,變量值一定要用雙引号括起來. key=$(get_key_of_entry "${entryline}") value=$(get_value_of_entry "${entryline}") # 經過驗證,當 key_values[] 後面跟着等号'='時,所給的[]不會進行 # 路徑名擴充,不需要像上面用 unset 指令移除數組元素那樣用雙引号 # 把整個數組元素括起來以避免路徑名擴充. key_values["${key}"]="${value}" # 下面是預留的調試語句.在調試的時候,可以打開下面的注釋. # echo "entryline=${entryline}" # echo "key=${key}" # echo "value=${value}" done < "${PARSECFG_filepath}" # 檢視關聯數組 key_values 的值.調試的時候,可以打開下面的注釋. # declare -p key_values}# 操作配置檔案的功能選項.建議外部調用者通過功能選項來指定要進行的操作.# 該函數最多接收兩個參數:# 第一個參數: 提供選項名,該選項名要求以'-'開頭,才是合法選項.# 第二個參數: 提供選項的參數. 部分選項後面需要跟着一個參數.# 當傳入的選項被handle_config_option()函數處理時,該函數傳回處理後的狀态碼.# 例如,處理成功傳回0,失敗傳回非0. 當傳入的選項不被該函數處理時,它傳回127.handle_config_option(){ if [ -z "${PARSECFG_filepath}" ]; then # 如果配置檔案變量值為空,說明還沒有解析配置檔案,不能往下處理. echo "出錯: 請先調用 open_config_file filename 來解析配置檔案." return 1 fi local option="$1" local argument="$2" case "${option}" in -l) show_config_file ;; -v) show_key_values ;; -i) search_value_from_file "${argument}" ;; -e) edit_config_file ;; -a) append_key_value "${argument}" ;; -d) delete_key_value "${argument}" ;; *) return 127 ;; esac # 當return語句不加上具體狀态碼時,它會傳回上一條執行指令的狀态碼. return}
使用 parsecfg.sh 腳本的例子
假設有一個 testparsecfg.sh 腳本,具體的代碼内容如下:
#!/bin/bashCFG_FILE="cfgfile.txt"# 通過 source 指令加載 parsecfg.sh 的腳本代碼source parsecfg.sh# 調用 open_config_file 函數解析配置檔案open_config_file "$CFG_FILE"# 調用 handle_config_option 函數處理 -v 選項.# 該選項以鍵值對的形式列出所有配置項.handle_config_option -v# 擷取 am 這個鍵名對應的鍵值value=$(get_value_by_key "am")echo "The value of 'am' key is: $value"# 使用 get_key_of_entry 函數從鍵值對中擷取鍵名.該函數# 針對鍵值對自身進行處理,所給的鍵值對可以不在配置檔案中.key=$(get_key_of_entry "a|adb logcat -b all")echo "The key of 'a|adb logcat -b' is: $key"
這個腳本所調用的函數都來自于 parsecfg.sh 腳本。
這個 testparsecfg.sh 腳本指定解析一個 cfgfile.txt 配置檔案。
該配置檔案的内容如下:
am|frameworks/base/services/core/java/com/android/server/am/w|frameworks/base/wifi/java/android/net/wifi/
把 parsecfg.sh 腳本、testparsecfg.sh 腳本、和 cfgfile.txt 配置檔案都放到同一個目錄下。
然後給這兩個腳本檔案都添加可執行權限。
執行 testparsecfg.sh 腳本,具體結果如下:
$ ./testparsecfg.shkey='am' value='frameworks/base/services/core/java/com/android/server/am/'key='w' value='frameworks/base/wifi/java/android/net/wifi/'The value of 'am' key is: frameworks/base/services/core/java/com/android/server/am/The key of 'a|adb logcat -b' is: a
可以看到,在 testparsecfg.sh 腳本中通過 source 指令引入 parsecfg.sh 腳本.
之後可以調用 parsecfg.sh 腳本裡面的代碼來解析配置檔案,非常友善。
如果多個腳本需要解析多個不同的配置檔案,可以在各自腳本中引入 parsecfg.sh 腳本,然後提供不同的配置檔案名即可。
