
使用Shell脚本提高测试效率
使用Shell脚本提高测试效率
还在手动重复那些枯燥的测试任务吗?作为一名有追求的测试工程师,我们应该让脚本替我们干重活!今天就来聊聊如何用Shell脚本把自己从重复劳动中解放出来,让测试工作变得更加高效和有趣。
背景:测试工程师的日常痛点
在日常的工作中,我们常常会遇到一些这样的一些情况:
- 一件工作,比较简单,但是可能需要批量处理数十个文件,从中抓取一些核心指标,生成一份数据分析文档;
- 每次测试前,我们可能都需要一个相同的环境,而这个环境,需要数十个步骤,周期性进行相同的测试;
真实的测试场景:
在做存储的性能测试时,需要对结果数据进行处理,其中有很多重复的工作需要完成,且如果人力一个个处理很可能出错。
那么如何编写一个简单的shell脚本来帮我们优化上述说的一些工作,提高我们测试效率呢?
本文分享一点编写shell脚本的思路以及awk、grep等工具的使用,希望能帮助大家解决部分工作过程中面临的一些问题。
知识铺垫:
grep
grep 是 Global Regular Expression Print(全局正则表达式打印)的缩写,它用于在文本文件中搜索匹配某个模式的行,并将匹配的行打印出来。
以下是 grep 命令的基本语法:
# 第一种形式
grep [option] [pattern] [file,file2]
# 第二种形式
command | grep [option] [pattern]核心选项:
-i:忽略大小写,不区分大小写地搜索。
-r:递归搜索,搜索指定目录下的所有文件和子目录。
-E:使用扩展正则表达式进行匹配。
-F: 按照字面的意思来匹配,即不支持正则表达式
-v:反向匹配,只打印不匹配模式的行。
-n:显示匹配行的行号。
-o: 只显示匹配的部分练习
这样一个文件
i love python
I love PYTHON
I LOVE pYtHoN常见用法
- 如何取到匹配的最后一个值
grep xxxx | tail -n 1- 如何取到匹配的倒数第二个值
grep xxxx | tail -n 2 | head -n 1- 使用正则扩展查找到需要的
grep -oE xxxxAwk
Awk 是一种强大的文本处理工具,它在 Unix/Linux 系统中广泛使用。它的名字是由其创始人 Alfred Aho、Peter Weinberger 和 Brian Kernighan 的姓氏首字母组成的。
Awk 的主要功能是逐行扫描文本文件,并根据用户指定的模式和动作来处理和操作数据。它的工作方式是将每行文本拆分成字段,并让用户定义的脚本对这些字段进行处理。
awk的基本语法模式:
# 第一种形式
awk "BEGIN{} pattern{commands}END{}" file
# 第二种形式
standard output | awk "BEGIN{} pattern{commands}END{}"- begin{} 正式处理数据之前执行的操作
- Pattern 匹配的模式,对初步处理的,进行一些二次处理,比如说数字的筛选文字的匹配之类的
- {commands} 处理的命令
- END{} 处理完左右匹配数据后执行的操作
awk还有一些内置的变量参数,常用的如下:
$0:当前行的内容。
$1, $2, ...:当前行的第一个字段、第二个字段等。
NF:当前行的字段数。即分割了多少分
NR:当前行的行号(从 1 开始)。 NR 表示从程序开始执行以来处理的总行数。
FNR:当前文件中的行号(从 1 开始)。FNR 表示当前文件内已处理的行数,对于每个新文件,FNR 会重置为 1。
FILENAME:当前处理的文件名。
FS:输入字段分隔符,不指定默认以空格或者tab按键分割举例说明:
# 打印passwd的文件
awk '{print $0}' /etc/passwd
# 打印第一个
awk -F":" '{ print $1 }' /etc/passwd
# 打印第一个和第三个
awk -F":" '{ print $1 $3 }' /etc/passwd
awk 'BEGIN{FS=":"}{ print $1,$3 }' /etc/passwd
# 打印每行的字段数和行数
awk 'BEGIN{FS=":"}{ print NF,NR }' /etc/passwd
awk对字符串的处理
| 函数 | 说明 | 实例 |
|---|---|---|
| gsub( Ere, Repl, [ In ] )sub | gsub 是全局替换( global substitution )的缩写。除了正则表达式所有具体值被替代这点,它和 sub 函数完全一样地执行。 | awk 'BEGIN { str = "Hello, World" print "String before replacement = " str gsub("World", "Jerry", str) print "String after replacement = " str }'Result String before replacement = Hello, World String after replacement = Hello, Jerry |
| match( String, Ere ) | 在 String 参数指定的字符串(Ere 参数指定的扩展正则表达式出现在其中)中返回位置(字符形式),从 1 开始编号,或如果 Ere 参数不出现,则返回 0(零)。RSTART和 RLENGTH是 awk内置变量,表示最近一次 match函数匹配的子字符串在原始字符串中的起始位置和长度。 | awk 'BEGIN { str = "One Two Three" subs = "Two" ret = match(str, subs) printf "Substring \"%s\" found at %d location.\n", subs, ret }'Result Substring "Two" found at 5 location. |
| substr( String, M, [ N ] ) | 返回具有 N 参数指定的字符数量子串。子串从 String 参数指定的字符串取得,其字符以 M 参数指定的位置开始。M 参数指定为将 String 参数中的第一个字符作为编号 1。如果未指定 N 参数,则子串的长度将是 M 参数指定的位置到 String 参数的末尾 的长度。 | awk 'BEGIN { str = "Hello, World !!!" subs = substr(str, 1, 5) print "Substring = " subs }'Result Substring = Hello |
数值计算及比较
计算
双小括号(( ))用于执行高级算术运算,并支持变量赋值。以下是一个示例:
result=$((5 + 3))
echo $result # 输出:8
在上述示例中,`$((5 + 3))`计算了5加3的结果,并将结果赋给变量`result`。"bc"是一种高精度计算器语言,可以进行数值运算和比较,支持任意精度的数值计算,包括浮点数。
result=$(echo "5 + 3" | bc)
echo "Result: $result"如果想控制精度
result=$(echo "scale=2; 10 / 3" | bc)
echo "Result: $result"比较
整数的比较,比较简单
eq 等于
gt 大于
lt 小于
num1="30"
num2="20"
if [ $num1 -gt $num2 ]; then
echo "num1 is greater than num2"
fi浮点数比较是一个比较头疼的事情,遇到的问题:常用的>,<,gt,lt 又没办法直接比较浮点数大小。
我采用的办法:
a=0.1
b=0.01
if (( $(echo "$a > $b" |bc -l) )); then
ehco "true"
else
echo "false"
fi
a=0.1
b=0.01
if [ $(echo "$a > $b"|bc) -eq 1 ];then
echo "true"
else
echo "false"
fi一点点正则
本次分享,重点不在于正则的使用,下面仅列举出几个常见的正则语法
| 语法 | 说明 | 表达式实例 | 输出 |
|---|---|---|---|
| . | 除了换行符的任意字符 | a.c | abcanc... |
| \ | 转义字符 | a.ca\c | a.ca\c |
| [...] | 字符集 | a[bcd]e | abeaceade |
| \d | 数字 | a\dc | a1c |
| \D | 非数字 | a\Dc | abc |
| * | 匹配前一个字符0或者无数次 | abc* | abc111abcdefpy* pY |
| ? | 匹配前一个字符0或者1次 | abc? | ababc |
| + | 匹配前一个字符1或者无数次 | abc+ | abccabcccc |
| () | 分组 | (abc)a(123 | 456)c |
项目实战
编写思路
- 分析需求:明确目标,进行拆解
- 先把自己要实现的功能抽象出来,形成一个个模块
- 也不需要想的特别全,在实现的过程中,没有实现的在补充
目标
在存储压力测试时,我们需要打高存储水位,并进行存储的fio压力测试。
测试完成,会生成若干个日志,我们我们需要提取各个fio文件中的IOPS bw 延迟等数据指标,判断本次压测测试是否
提取的数据会汇总成如下的表格:
| 存储水位 | 测试项 | 子项 | 指标 | IOPS/吞吐 | IO时延 | 结论 |
|---|---|---|---|---|---|---|
| 2% | IOPS | 随机读4K | 时延 < 1000ms | IOPS=1362k | lat (msec) 15.0139 | |
| 随机写4K | 时延 < 1000ms | IOPS=559k | lat (msec) 36.6147 | |||
| 随机读写4K | 时延 < 1000ms | read: IOPS=408kwrite: IOPS=408k | read: lat (msec) 17.168write: lat (msec) 32.9405 | |||
| 吞吐 | 随机读1M | >= 2.5GiB/s * 存储节点数 | BW=31.5Gi | lat (msec) 622.19 | ||
| 随机写1M | >= 2.5GiB/s * 存储节点数 / 2 | BW=15.8Gi | lat (msec) 1260.55 | |||
| 随机读写1M | read: 10.8 Giwrite: 10.8 Gi | read: lat (msec) 688.94write: lat (msec) 1149.79 | ||||
| 25% | IOPS | 随机读4K | 时延 < 1000ms | IOPS=1344k | lat (msec) 15.237 | |
| 随机写4K | 时延 < 1000ms | IOPS=513k | lat (msec) 39.9454 | |||
| 随机读写4K | 时延 < 1000ms | read: IOPS=399kwrite: IOPS=399k | read: lat (msec) 17.5188write: lat (msec) 33.7072 | |||
| 吞吐 | 随机读1M | >= 2.5GiB/s * 存储节点数 | BW=31.2Gi | lat (msec) 630.86 | ||
| 随机写1M | >= 2.5GiB/s * 存储节点数 / 2 | BW=15.0Gi | lat (msec) 1252.19 | |||
| 随机读写1M | read: 10.8 Giwrite: 10.7 Gi | read: lat (msec) 690.15write: lat (msec) 1149.04 |
拆分
本次拆分的功能:提取的数据部分
我们将上述需求进行拆分:
- fio数据结果存放在logs文件夹中,脚本会遍历该文件夹,一一读取分析
- 需要根据文件名提取存储水位、测试项等指标,文件的命名 fio_{water_size}{bw}.log
- 根据上面提取的值,判断这个log文件中,需要提取哪些关键指标
- bw=4k
- 随机读 提取read的iops值
- 随机写 提取write的iops值
- 随机读写 提取read write的iops值
- bw=1m
- 随机读 提取read的bw值
- 随机读 提取write的bw值
- 随机读写 提取read write的bw值
- 单位统一
- 带宽数据得到的值统一为Gi
- IOPS的值统一为k
- 延迟数据的提取
- bw=4k
- 判断本次测试的这些值是否符合预期
- 时延 < 1000ms
= 2.5GiB/s * 存储节点数
- 将结果输出到csv文件中
核心函数
- 函数入口:
- 传参:log_dir,output_dir
- 逻辑:创建output_dir文件,遍历logs文件,对logs文件夹里面每个文件进行解析
function get_result() {
local log_dir=${1}
local output_dir=${2}
# 判断有没有output_dir,如果没有则创建
create_file "${output_dir}"
# 向文件中写入标题
echo "water,testcase,data,lat,result" >>"${output_dir}"
# 遍历log_dir目录中的文件
for file in $(ls "$log_dir" | sort -r); do
get_result_single "${log_dir}/${file}" "${output_dir}"
done
# 输出最终结果
}- 对每个文件进行数据提取,并将结果存储到
output_dir中
function get_result_single() {
local file_dir=$1
local output_dir=${2}
split_filename "${file_dir}"
if [ "${parts[3]}" = "rw" -a "${parts[2]}" = "1M" ]; then ## mode:rw bw:1m
## 使用BW随机读写数据分析函数
get_fio_BW_randrw_result "${file_dir}"
result=none
elif [ "${parts[3]}" = "rw" -a "${parts[2]}" = "4k" ]; then ## mode:rw bw:4k
## 使用BW 随机读写数据分析函数
get_fio_IOPS_randrw_result "${file_dir}"
result=none
elif [ "${parts[3]}" = "r" -a "${parts[2]}" = "1M" ] || [ "${parts[3]}" = "w" -a "${parts[2]}" = "1M" ]; then ## mode:r/w bw:1m
get_fio_BW_randr_or_w_result "${file_dir}"
# 如果带宽小于2.5GiB/s * 存储节点数,则测试失败
# 获取bw的值
bw_data=$(echo "$lat" | grep -oE '[0-9]+([.][0-9]+)?')
echo $bw_data "bw"
if [ $(echo "$bw_data >= 2.5" | bc -l) -lt 1 ]; then
result="fail"
else
result="pass"
fi
elif [ "${parts[3]}" = "r" -a "${parts[2]}" = "4k" ] || [ "${parts[3]}" = "w" -a "${parts[2]}" = "4k" ]; then ## mode:r/w bw:4k
get_fio_BW_randr_or_w_result "${file_dir}"
# 获取延迟的值
lat_data=$(echo "$lat" | grep -oE '[0-9]+([.][0-9]+)?')
echo $lat_data "lat"
# 如果延迟大于1000毫秒,则测试失败
if [ $(echo "$lat_data > 1000" | bc -l) -gt 1 ]; then
result="fail"
else
result="pass"
fi
fi
echo "${parts[1]}",bw="${parts[2]}"\;mode="${parts[3]}",${bw},${lat},"${result}" >>"${output_dir}"
}- 根据文件名,对不同的文件进行不同的数据提取方式
## fio BW 随机读写文件,数据提取,需传入 文件地址
function get_fio_BW_randrw_result() {
local file_dir=${1}
bw=$(get_BW_2 "${file_dir}")
lat=$(get_lat_2 "${file_dir}")
}
## fio IOPS 随机读写文件,数据提取,需传入 文件地址
function get_fio_IOPS_randrw_result() {
local file_dir=${1}
bw=$(get_IOPS_2 "${file_dir}")
lat=$(get_lat_2 "${file_dir}")
}
## fio BW 随机读或者随机写文件,数据提取,需传入 文件地址
function get_fio_BW_randr_or_w_result() {
local file_dir=${1}
bw=$(get_BW "${file_dir}")
lat=$(get_lat "${file_dir}")
}
## fio IOPS 随机读或者随机写文件,数据提取,需传入 文件地址
function get_fio_IOPS_randr_or_w_result() {
local file_dir=${1}
bw=$(get_IOPS "${file_dir}")
lat=$(get_lat "${file_dir}")
}- 对IOPS进行数据提取
function get_IOPS() {
local file_dir=$1
# 输出结果的最后一行
grep -oE 'IOPS=[^, ]+' "$file_dir" | awk 'END{print}'
}
## 提取fio文件随机读写IOPS的值
function get_IOPS_2() {
local file_dir=$1
## 输出匹配到的倒数第二行结果,读的结果
echo 'read:' $(grep -E 'IOPS=[^, ]+' "$file_dir" | tail -n 2 | head -n 1 | grep -oE 'IOPS=[^, ]+')
## 输出匹配到的最后一行结果,写的结果
echo 'write:' $(grep -E 'IOPS=[^, ]+' "$file_dir" | tail -n 1 | grep -oE 'IOPS=[^, ]+')
}- 对bw进行数据提取
## 读取fio文件中BW的相关数值,写入到${resulttxt}文本中
## 格式:文件名 \n 数据
## grep -oP 'BW=\K[^, ]+' /data/fio-a.log
function get_BW() {
local file_dir=$1
grep -oE 'BW=[^, ]+' "$file_dir" | tail -n 1 | awk -F= '{
match($2, /^[0-9]+(\.[0-9]+)?/)
number = substr($2, RSTART, RLENGTH)
unit = substr($2, RLENGTH + 1)
if (unit == "Mi") {
number = number / 1024
unit = "Gi"
}
print "BW="number , unit
}'
}
## 提取fio文件随机读写BW的值
function get_BW_2() {
local file_dir=$1
## 输出匹配到的倒数第二行结果,读的结果
res1=$(grep -oE 'BW=[^, ]+' "$file_dir" | tail -n 1 | awk -F= '{
match($2, /^[0-9]+(\.[0-9]+)?/)
number = substr($2, RSTART, RLENGTH)
unit = substr($2, RLENGTH + 1)
if (unit == "Mi") {
number = number / 1024
unit = "Gi"
}
print number , unit
}')
res2=$(grep -oE 'BW=[^, ]+' "$file_dir" | tail -n 2 | head -n 1 | awk -F= '{
match($2, /^[0-9]+(\.[0-9]+)?/)
number = substr($2, RSTART, RLENGTH)
unit = substr($2, RLENGTH + 1)
if (unit == "Mi") {
number = number / 1024
unit = "Gi"
}
print number , unit
}')
echo 'read:BW='"${res1}"
## 输出匹配到的最后一行结果,写的结果
echo 'write:BW='"${res2}"
}- 对延迟进行数据提取,提取的数据全部转换为msec
## 读取fio文件中lat的相关数值,写入到${resulttxt}文本中
## 格式:文件名 \n 数据
## grep -oP 'lat \([^)]+\): min=[0-9]+, max=[0-9]+, avg=[0-9.]+, stdev=[0-9.]+' /data/fio-a.log | awk -F'[:,=]' 'END{print $1, $7}'
function get_lat() {
local file_dir=$1
grep -oE '^[[:space:]]*lat \([^)]+\): min=[0-9]+(\.[0-9]+)?[kK]?, max=[0-9]+(\.[0-9]+)?[kK]?, avg=[0-9]+(\.[0-9]+)?[kK]?, stdev=[0-9]+(\.[0-9]+)?[kK]?' "$file_dir" | tail -n 1 | awk -F'[:,=]' '{if ($1 ~ /usec/) { $7 = $7 / 1000; gsub(/usec/, "msec", $1) } print $1, $7}'
}
## 读取fio随机读写类型的文件中lat的相关数据,写入到指定文本中
function get_lat_2() {
local file_dir=$1
echo 'read='$(grep -oE '^[[:space:]]*lat \([^)]+\): min=[0-9]+(\.[0-9]+)?[kK]?, max=[0-9]+(\.[0-9]+)?[kK]?, avg=[0-9]+(\.[0-9]+)?[kK]?, stdev=[0-9]+(\.[0-9]+)?[kK]?' "$file_dir" | tail -n 2 | head -n 1 | awk -F'[:,=]' '{if ($1 ~ /usec/) { $7 = $7 / 1000; gsub(/usec/, "msec", $1) } print $1, $7}')
echo 'write='$(grep -oE '^[[:space:]]*lat \([^)]+\): min=[0-9]+(\.[0-9]+)?[kK]?, max=[0-9]+(\.[0-9]+)?[kK]?, avg=[0-9]+(\.[0-9]+)?[kK]?, stdev=[0-9]+(\.[0-9]+)?[kK]?' "$file_dir" | tail -n 1 | awk -F'[:,=]' '{if ($1 ~ /usec/) { $7 = $7 / 1000; gsub(/usec/, "msec", $1) } print $1, $7}')
}输出结果
最终结果展示:

总结与思考
通过这个实际项目,我们学会了:
🎯 核心技能
- grep + awk + sed 组合拳:文本处理的黄金搭档
- 正则表达式:精确匹配我们需要的数据
- 函数化编程:提高代码复用性和可维护性
- 数据格式化:统一单位,便于分析比较
💡 实战经验
- 模块化思维:将复杂问题拆分成小模块逐个击破
- 错误处理:考虑边界情况,让脚本更健壮
- 可读性优先:清晰的变量命名和注释比炫技更重要
🚀 扩展思路
这个脚本还可以进一步优化:
# 1. 添加配置文件支持
source config/test_config.conf
# 2. 增加邮件通知功能
send_email_notification() {
local result_file=$1
echo "测试完成,结果已生成:$result_file" | mail -s "性能测试报告" team@company.com
}
# 3. 集成到CI/CD流水线
if [ "$CI" = "true" ]; then
# 在CI环境中的特殊处理
upload_to_artifact_storage "$output_file"
fi🎉 写在最后
Shell脚本的魅力就在于此:用简单的工具解决复杂的问题。作为测试工程师,掌握这些技能不仅能提高工作效率,更能让我们在团队中发挥更大的价值。
记住:好的脚本不是写出来的,是改出来的。多实践、多思考、多优化,你也能写出优雅高效的测试脚本!
