本篇文章介紹一個解析、以及增刪改查鍵值對格式配置文件的 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 threadtime
png|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' t"
printf "value='e[33m${key_values["${key_name}"]}e[0m'n"
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/bash
CFG_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.sh
key='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 腳本,然后提供不同的配置文件名即可。