AWK技巧:如何按空格分割但不拆分引号内的字段?
2025-03-25 17:49:53
用 AWK 处理带引号的字段:空格分割,但引号内除外
处理文本数据时,经常会遇到需要按特定分隔符(比如空格)拆分字段的情况。但如果文本里包含了带引号的字符串,而引号内的内容也包含分隔符,事情就变得有点棘手了。咱不希望引号里的内容被错误地拆开。
问题来了:空格分割遇上引号
假设有这样一个配置文件 argstext
:
Pipelines:
YOLO-Goby
Pipeline Container Arguments:
Bubblerargs:
python Collect_Unpack.py --primary_images --processes 1 --every_nth 25 --output_folder
YOLOargs:
python 03_YOLO_infer_no_labels.py --img_list_txt --output_name inference_output_test --confidence 0.1 --weights "/mnt/c/Users/jmilitello/OneDrive - DOI/ARIS_MLM_Files/Literature/Literature for Pete/Code/best.pt"
目标是提取 YOLOargs:
下面那行的参数,放到 Bash 数组里。参数之间用空格隔开,但那个带引号的文件路径 "/mnt/c/Users/.../best.pt"
应该被看作一个整体 ,数组的一个元素,而不是被空格拆成好几块。
如果直接用 awk
按空格分割,就像这样:
# 提取 YOLOargs 下一行的内容
line_content=$(awk '/YOLOargs:/{getline; print}' argstext)
# 尝试直接放入数组
YOLOcontainerargs=($line_content)
# 查看数组内容
declare -p YOLOcontainerargs
你会发现数组 YOLOcontainerargs
里面的文件路径被空格无情地分割了:
declare -a YOLOcontainerargs=([0]="python" [1]="03_YOLO_infer_no_labels.py" [2]="--img_list_txt" [3]="--output_name" [4]="inference_output_test" [5]="--confidence" [6]="0.1" [7]="--weights" [8]="\"/mnt/c/Users/jmilitello/OneDrive" [9]="-" [10]="DOI/ARIS_MLM_Files/Literature/Literature" [11]="for" [12]="Pete/Code/best.pt\"")
# 注意看元素 8 到 12,路径被拆开了
这显然不是咱们想要的结果。
为啥会这样?AWK 的默认分割逻辑
awk
工作的时候,默认会把空格、制表符这类空白字符当作字段分隔符(FS
, Field Separator)。它一行一行地读入数据,然后根据 FS
把行切成一个个字段,存到 $1
, $2
, $3
这些变量里。
当 awk
遇到 ... --weights "/mnt/c/.../best.pt"
这一部分时,它眼里的分隔符就是空格。所以 "/mnt/c/Users/jmilitello/OneDrive
、-
、DOI/ARIS_MLM_Files/Literature/Literature
等等都被认为是独立的字段。awk
默认情况下并不理解引号的特殊含义,不会把引号括起来的内容当作一个整体来保护。
怎么办?几种 AWK 搞定方法
要让 awk
聪明地处理引号,我们需要换个思路,不能光靠默认的 FS
。
方法一:GNU AWK 的 FPAT
大显身手 (推荐)
如果你用的是 GNU Awk (通常就是 Linux 发行版和 WSL 里的 awk
,可以通过 awk --version
查看,带有 GNU Awk
字样),那就有个很方便的特性:FPAT
(Field Pattern)。
FS
定义的是 分隔符 是什么样子,而 FPAT
定义的是 字段本身 是什么样子。这就好办了!我们可以定义字段要么是“一串不包含空格和双引号的字符”,要么是“一个由双引号包围的字符串”。
原理:
FPAT
接受一个正则表达式。awk
会用这个表达式去匹配输入行,每次成功匹配到的内容就作为一个字段。
- 匹配不含空格或引号的字段:
[^[:space:]"]+
[^...]
: 匹配不在括号内的任意字符。[:space:]
: 代表所有空白字符(空格、tab 等)。"
: 就是双引号字符。+
: 表示匹配前面这个规则一次或多次。- 合起来就是:匹配一个或多个既不是空白字符也不是双引号的字符。
- 匹配双引号包围的字段:
"[^"]*"
"
: 匹配开头的双引号。[^"]
: 匹配任意不是双引号的字符。*
: 匹配前面这个规则零次或多次(允许空字符串""
)。"
: 匹配结尾的双引号。- 合起来就是:匹配一个由双引号包围,内部不含双引号的字符串。
把这两种模式用 |
(或) 连接起来,就是 FPAT
的值:FPAT = "([^[:space:]\"]+)|(\"[^\"]*\")"
。
代码/步骤:
- 找到目标行: 仍然可以用之前的
/YOLOargs:/
模式来定位。 - 设置
FPAT
并处理: 在找到目标行后,用getline
读取下一行,然后设置FPAT
并遍历所有字段 ($1
到$NF
)。
# fpat_parser.awk
/YOLOargs:/ {
# 读下一行
getline
# 设置 FPAT 规则
FPAT = "([^[:space:]\"]+)|(\"[^\"]*\")"
# 遍历所有通过 FPAT 匹配到的字段
for (i = 1; i <= NF; i++) {
# 打印每个字段,用一个特殊的分隔符(比如换行符或 \0)
# 以便 Bash 能准确地读入数组,这里用换行符演示
print $i
}
exit # 处理完就退出,避免处理其他行
}
在 Bash 中使用:
# 先用 awk 提取并按正确方式分割字段,每行一个字段
# 注意:这里需要 gawk (GNU Awk)
fields_output=$(gawk -f fpat_parser.awk "${argstext}")
# 使用 mapfile (或 readarray) 将 awk 的输出读入 Bash 数组
# 这是最安全的方式,能正确处理包含空格的元素
mapfile -t YOLOcontainerargs < <(printf '%s\n' "$fields_output")
# 查看结果
declare -p YOLOcontainerargs
执行后,数组内容就是期望的样子了:
declare -a YOLOcontainerargs=([0]="python" [1]="03_YOLO_infer_no_labels.py" [2]="--img_list_txt" [3]="--output_name" [4]="inference_output_test" [5]="--confidence" [6]="0.1" [7]="--weights" [8]="\"/mnt/c/Users/jmilitello/OneDrive - DOI/ARIS_MLM_Files/Literature/Literature for Pete/Code/best.pt\"")
# 注意元素 8,带引号的路径现在是一个完整的元素了!
安全建议:
FPAT
的正则表达式对于嵌套引号或包含转义引号(\"
)的场景处理起来会更复杂,上面的模式只处理了简单情况。如果输入源不可控,可能包含复杂的引用结构,这种简单正则可能会失效或出错。- 始终考虑输入数据的来源和格式。如果可能,对输入进行预处理或校验,确保其符合预期的模式。
进阶使用技巧:
FPAT
可以写得更健壮,比如处理单引号'...'
或处理转义字符\
。例如,一个更复杂的FPAT
可能需要处理\\"
或\\'
。- 如果性能是关键,对于超大文件,
FPAT
相较于某些手动解析方法通常效率更高,因为它利用了awk
底层的优化 regex 引擎。 - 需要注意
FPAT
是 GNU Awk 特有的,如果你的脚本需要在只安装了 POSIXawk
(比如 macOS 或某些老旧 Unix 系统上的默认awk
) 的环境运行,这个方法就不行了。
方法二:手动解析循环和 match
函数 (更通用,稍复杂)
如果不能用 FPAT
(比如环境限制),或者想了解更底层的处理方式,可以手动编写 awk
代码来解析。基本思路是遍历字符串,判断当前是在引号内还是引号外,然后据此决定如何分割。
原理:
- 找到
YOLOargs:
的下一行。 - 使用循环和
awk
的字符串函数(如match
,substr
)来扫描这一行。 - 维护一个状态变量,标记当前是否在引号内部。
- 遇到非引号、非空格字符时,如果不在引号内,开始积累一个字段;如果遇到空格且不在引号内,标志一个字段结束。
- 遇到引号时,切换状态,并将引号内的所有内容(直到下一个匹配的引号)当作一个字段。
这种方法逻辑比较绕,容易出错。用 match
函数寻找下一个引号或下一个空白,然后用 substr
提取片段,会稍微简洁一点。
代码/步骤:
下面是一个使用 match
和 substr
的简化版手动解析逻辑:
# manual_parser.awk
function parse_quoted_line(line, parts, # 输出数组
pos, len, rstart, rlength, field, in_quotes) {
pos = 1 # 当前处理位置
len = length(line)
delete parts # 清空输出数组
while (pos <= len) {
# 跳过开头的空格
while (substr(line, pos, 1) == " ") {
pos++
}
if (pos > len) break # 到行尾了
in_quotes = 0
if (substr(line, pos, 1) == "\"") { # 引号开头
in_quotes = 1
pos++ # 跳过开头的引号
# 寻找下一个不在转义符后的引号
# 简单版本:直接找下一个引号
if (match(substr(line, pos), /[^"]*/)) {
field = "\"" substr(line, pos, RLENGTH) "\"" # 包含引号
pos += RLENGTH + 1 # 跳过内容和结尾引号
} else { # 没找到结尾引号,视为错误或取到行尾
field = "\"" substr(line, pos) # 包含开头引号,直到行尾
pos = len + 1
}
} else { # 非引号开头
# 寻找下一个空格或引号
if (match(substr(line, pos), /[^ "]+/)) {
field = substr(line, pos, RLENGTH)
pos += RLENGTH
} else { # 应该不会到这里,除非行尾有空格?
break
}
}
parts[length(parts) + 1] = field # 添加到数组
}
}
/YOLOargs:/ {
getline current_line
parse_quoted_line(current_line, fields_array)
for (i = 1; i <= length(fields_array); i++) {
print fields_array[i] # 同样,每行打印一个字段
}
exit
}
在 Bash 中使用(与方法一类似):
# 使用 awk 执行手动解析脚本
fields_output=$(awk -f manual_parser.awk "${argstext}")
# 同样使用 mapfile 读入数组
mapfile -t YOLOcontainerargs < <(printf '%s\n' "$fields_output")
# 查看结果
declare -p YOLOcontainerargs
输出结果应该与使用 FPAT
的方法相同。
安全建议:
- 手动解析代码比
FPAT
更容易出 bug,尤其是在处理边缘情况时(如空字符串、连续空格、转义引号、未闭合的引号等)。上面示例代码是简化的,并没有处理所有复杂情况。 - 务必充分测试手动解析的脚本,覆盖各种可能的输入。
进阶使用技巧:
- 可以增强
parse_quoted_line
函数来处理转义引号\"
。这通常需要更细致地检查引号前面的字符是不是反斜杠。 - 性能方面,纯
awk
脚本实现的循环解析通常比 C 语言实现的FPAT
慢,但对于不是特别长的行或不是海量数据,差异可能不明显。 - 这种手动方法的好处是它不依赖 GNU Awk 特性,在更多版本的
awk
上也能运行。
整合到 Bash 脚本
前面两个方法的核心都是让 awk
正确地输出字段,每个字段占一行。然后 Bash 这边使用 mapfile
(或者别名 readarray
) 把这些行读入数组。
#!/bin/bash
argstext="argstext" # 假设配置文件名叫这个
# 创建 argstext 文件用于测试
cat > "${argstext}" << EOF
Pipelines:
YOLO-Goby
Pipeline Container Arguments:
Bubblerargs:
python Collect_Unpack.py --primary_images --processes 1 --every_nth 25 --output_folder
YOLOargs:
python 03_YOLO_infer_no_labels.py --img_list_txt --output_name inference_output_test --confidence 0.1 --weights "/mnt/c/Users/jmilitello/OneDrive - DOI/ARIS_MLM_Files/Literature/Literature for Pete/Code/best.pt"
EOF
# --- 使用 FPAT (需要 gawk) ---
gawk_script='
/YOLOargs:/ {
getline
FPAT = "([^[:space:]\"]+)|(\"[^\"]*\")"
for (i = 1; i <= NF; i++) {
# 为确保 mapfile 正确处理,可以移除字段两端的引号(如果需要裸值)
# current_field = $i
# gsub(/^"|"$/, "", current_field) # 取消注释此行以移除引号
# print current_field
print $i # 保持原样,带引号
}
exit
}'
# 执行 gawk 并用 mapfile 读取
mapfile -t YOLOcontainerargs_gawk < <(gawk "$gawk_script" "${argstext}")
echo "使用 FPAT 的结果:"
declare -p YOLOcontainerargs_gawk
# --- 使用手动解析 (通用 awk) ---
manual_awk_script='
function parse_quoted_line(line, parts, pos, len, rstart, rlength, field, in_quotes) {
pos = 1; len = length(line); delete parts
while (pos <= len) {
while (substr(line, pos, 1) == " ") { pos++ }
if (pos > len) break
if (substr(line, pos, 1) == "\"") {
in_quotes = 1; pos++
# 简化版找下一个引号
match_len = match(substr(line, pos), /[^"]*/)
field = "\"" substr(line, pos, match_len) "\""
pos += match_len + 1
} else {
match_len = match(substr(line, pos), /[^ "]+/)
field = substr(line, pos, match_len)
pos += match_len
}
parts[length(parts) + 1] = field
}
# 将结果填充回关联数组(或者直接用函数参数传递)
for(i=1; i<=length(parts); ++i) _global_parts[i] = parts[i]; _global_parts_len = length(parts);
}
/YOLOargs:/ {
getline current_line
# 调用函数,结果存放在 _global_parts
parse_quoted_line(current_line, _global_parts)
for (i = 1; i <= _global_parts_len; i++) {
# gsub(/^"|"$/, "", _global_parts[i]) # 可选:移除引号
print _global_parts[i]
}
exit
}'
# 执行通用 awk 并用 mapfile 读取
mapfile -t YOLOcontainerargs_manual < <(awk "$manual_awk_script" "${argstext}")
echo "使用手动解析的结果:"
declare -p YOLOcontainerargs_manual
# 清理测试文件
# rm "${argstext}"
这个脚本演示了如何将 awk
代码嵌入 Bash,并通过 mapfile
安全地创建数组。你可以根据需要选择是否移除提取出字段两端的引号。对于那个文件路径,保留引号通常更安全,避免 Bash 后续处理时因路径中的空格再次产生问题。
相关资源
- GNU Awk 用户手册中关于
FPAT
的章节: GNU Awk Manual - Defining Fields by Content