POSIX Shell下生成符合转义规则的TSV记录的实现及相关问题咨询
原问题背景
我正在编写一个POSIX Shell函数,功能是将传入的参数输出为一条TSV记录,每个参数需要遵循以下转义规则:
- 换行符转义为
\n - 制表符转义为
\t - 回车符转义为
\r - 反斜杠转义为
\\
现有实现代码
预定义的特殊字符(原代码可读性较差):
__TAB__=$(printf '\t') __CR__=$(printf '\r') __LF__=" "
函数实现:
#!/bin/sh tsv_print() { rec= for str in "$@" do esc= i=${#str} until [ $i -eq 0 ] do end="${str#?}" chr="${str%"$end"}" case $chr in "$__TAB__") chr='\t' ;; "$__LF__") chr='\n' ;; "$__CR__") chr='\r' ;; \\) chr='\\' ;; esac esc="$esc$chr" str="${end}" i=$((i-1)) done rec="$rec${rec:+"$__TAB__"}$esc" done # echo "$rec" printf '%s\n' "$rec" }
待确认问题
- 原代码字符转义不生效问题已解决:经@GordonDavisson指出,原使用
echo输出是问题根源,使用printf是唯一可移植的实现方式,代价是可能产生fork调用。 - 是否存在更优的符合POSIX规范的实现方案?
awk和sed看起来不太适合该场景。 - 如何实现对应的转义还原操作?经@KamilCuk在相关回答中提到,使用
printf '%b'即可实现还原,该TSV记录格式天然适配该命令的输入要求。
补充说明
最终该函数未投入使用,因为实际输入没有需要转义的字符。实际输入为STAR File格式,格式转换难度较高:每行的列数不固定(单行长限制为80字符)且包含带引号的字符串,输入输出示例如下:
输入示例
... loop_ _refl_0201 _refl_0012 _refl_2003 _refl_1600 _refl_1304 _refl_1305 _refl_1800 _refl_1801 _refl_1802 _refl_1803 _refl_1804 _refl_1805 _refl_1806 _refl_1701 _refl_1700 _refl_1202 '0 0 6' .147364 Z000020c1 .41 1 78.45 3.501 35.2221 -35.2221 0 -1.6055 -3.0963 -36.7288 -5.0964 39.3109 5.909983 '0 0 12' .294551 Z000010c1 .9 1 48.44 2.3805 39.910008 39.9101 .268379-04 1.75598 3.09745 41.6656 3.09809 47.8384 0 .939517 ...
输出示例(分隔符为制表符)
_refl_0201 _refl_0012 _refl_2003 _refl_1600 _refl_1304 _refl_1305 _refl_1800 _refl_1801 _refl_1802 _refl_1803 _refl_1804 _refl_1805 _refl_1806 _refl_1701 _refl_1700 _refl_1202 '0 0 8' .147364 Z000020c1 .41 1 78.45 3.501 35.2221 -35.2221 0 -1.6055 -3.0963 -36.7288 -5.0964 39.3109 5.909983 '0 0 14' .294551 Z000010c1 .9 1 48.44 2.3805 39.910008 39.9101 .268379-04 1.75598 3.09745 41.6656 3.09809 47.8384 0.939517 ...
问题解答
1. 转义输出方案确认
你选择printf替代echo的方案完全正确,POSIX规范未定义echo对反斜杠转义的默认处理逻辑,不同平台实现差异极大,printf '%s\n'是唯一可移植的输出方案。另外你提到的fork开销基本可以忽略:目前绝大多数主流POSIX Shell(bash、dash、ksh等)都内置了printf命令,不会产生额外的进程调用,只有极其老旧的Shell才会调用外部printf二进制文件。
2. 更优的POSIX兼容实现方案
你现有的纯Shell逐字符遍历方案兼容性最高,无任何外部依赖,适合资源受限或兼容性要求极高的场景。如果可以接受调用POSIX兼容的sed,可以使用以下实现,处理长字符串时性能提升非常明显:
tsv_print() { [ $# -eq 0 ] && return # 处理第一个字段,无需前置制表符 printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/\t/\\t/g' -e 's/\r/\\r/g' -e ':a' -e '/\n/{N;ba' -e '}' -e 's/\n/\\n/g' shift # 处理剩余字段,每个前置制表符 for str do printf '\t' printf '%s' "$str" | sed -e 's/\\/\\\\/g' -e 's/\t/\\t/g' -e 's/\r/\\r/g' -e ':a' -e '/\n/{N;ba' -e '}' -e 's/\n/\\n/g' done printf '\n' }
如果你需要完全避免外部命令,可对你的现有代码做小幅优化:把特殊字符的定义放在函数内部,避免污染全局命名空间,同时添加注释提升可读性即可。
3. 转义还原实现说明
printf '%b'确实是最适配的还原方案,%b格式说明符会把输入字符串中的反斜杠转义序列展开为对应的实际字符,完全匹配你定义的转义规则。还原时注意按行读取TSV记录,逐行处理即可,参考实现如下:
tsv_parse() { # 输入为单条TSV转义记录,输出为原始字段,可通过$@获取 local ifs_bak=$IFS IFS=$__TAB__ # 关闭glob避免字段中包含通配符时意外展开 set -f set -- $(printf '%b' "$1") set +f IFS=$ifs_bak # 后续可直接处理$@中的原始字段 for arg do printf '%s\n' "$arg" done }
STAR File格式转换提示
针对你提到的STAR File格式,纯Shell处理的核心思路是:
- 先读取所有行,去除行首前导空格,把多行拼接为单个长字符串
- 解析时按单引号区分字段:遇到单引号就把到下一个单引号之间的所有内容作为单个字段,非单引号开头的内容按空格拆分字段
- 收集到足够的字段数后输出为一行TSV,然后继续解析剩余内容
内容的提问来源于stack exchange,提问作者Fravadona

