关于linux:Bash技巧介绍一个可以增删改查键值对格式配置文件的Shell脚本

25次阅读

共计 8964 个字符,预计需要花费 23 分钟才能阅读完成。

本篇文章介绍一个解析、以及增删改查键值对格局配置文件的 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/ 命令,进入到对应的目录下。

这样就不须要输出多个字符,十分不便。

前面的文章会介绍在不同目录之间疾速来回 cdquickcd.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 separator

    set_info_ifsparsecfg.sh 脚本外面的函数,用于设置分隔符。

    所给的第一个参数指定新的分隔符。

    默认分隔符是 |。如果须要解析的配置文件用的是其余分隔符,就须要先设置分隔符,再解析配置文件。

    如果应用默认的分隔符,能够跳过这个步骤。

  • open_config_file filename

    open_config_fileparsecfg.sh 脚本外面的函数,用于解析配置文件。

    所给的第一个参数指定配置文件名。

  • (可选的)handle_config_option -l|-v|-i|-e|-a|-d

    handle_config_optionparsecfg.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 脚本,而后提供不同的配置文件名即可。

正文完
 0