【问题标题】:POSIX shell function: printing arguments as a TSV recordPOSIX shell 函数:将参数打印为 TSV 记录
【发布时间】:2021-12-31 01:06:48
【问题描述】:

我正在编写一个 POSIX shell 函数,它将其参数打印为 TSV 记录。
每个参数都使用以下规则进行转义:

  • \n 换行
  • \t 标签页
  • \r 用于回车
  • \\ 用于反斜杠

函数如下:

#!/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"
}

在代码中读起来很痛苦——预先存储的字符如下:

__TAB__=$(printf '\t')
__CR__=$(printf '\r')
__LF__="
"

我想知道:

  1. 为什么我的代码根本不转义字符?

    edit: 正如@GordonDavisson 指出的那样,echo 是 罪魁祸首!!使用printf 似乎是唯一可移植的方式,但代价是可能的分叉。

  2. 有更好的、符合 POSIX 的方法吗? awksed 似乎不适合这份工作......

  3. 你会怎么做?

    编辑:正如@KamilCuk 在他的answer 中发布的那样,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
...

【问题讨论】:

  • 我认为您的 echo 命令可能正在解释转义序列。请改用printf '%s\n' "$rec"
  • 你写超级POSIX兼容脚本有什么具体原因吗?
  • 我理解您的帖子的方式是您的数据嵌入了转义字符,您试图将其转换为“真实”“格式”(制表符等)字符?如果是,那么,这就是疯狂之路。希望一关。不要接受格式错误的提要!否则编辑您的 Q 以显示准确的示例输入文本和所需的输出。祝你好运。
  • $(...) 将始终去除尾随的换行符。但是你不需要它来设置__LF__;你可以简单地输入__LF__=",回车,然后用"完成命令。 __TAB____CR__ 不需要这样的技巧:单独使用 __TAB__=$(printf '\t') 就足够了,__CR__ 也是如此。
  • @KamilCuk 目标系统是旧的 SGI IRIX,我不确定它的 bourne shell 的功能。我的一位退休同事让我在上面运行他的一个 super 程序(当时确实是这样^^),所以我必须将数据复制到外部 SCSI 磁盘(连同使用我的脚本),转到计算机,使用数据运行第一个程序,将其输出转换为 TSV,然后运行目标程序。我的同事告诉我,我必须将伪 CSV(没有转义,但包含标签)转换为有效的 TSV。他自己当时是手动做的……

标签: escaping sh posix


【解决方案1】:

您可以使用sed 来完成此操作,但需要一些技巧才能使其工作。

您需要先转义“\”,这样“\t”、“\n”和“\r”中的反斜杠就不会被转义。

sed 每行有效,但您可以命令它在每行末尾添加“\n”,并使用单独的命令删除换行符。

sed 将字符串的最后一行视为相同,无论它是否以 "\n" 结尾但添加 '.'最后,它的输入并删除输出的最后 3 个字符就可以了。

此解决方案比 shell 循环快得多。 (基本上,一切都比 shell 快。)

tsv_print() {
    is_first_arg='yes'
    while [ $# -ne 0 ]
    do
        test "$is_first_arg" = 'yes' && is_first_arg='no' || printf '\t'
        printf '%s.' "$1" \
        | sed -e 's/\\/\\\\/g' \
            -e 's/\t/\\t/g' \
            -e 's/$/\\n/' \
            -e 's/\r/\\r/g' \
        | tr -d '\n' \
        | head -c -3
        shift
    done
}

顺便说一句,有更简单的方法来定义这些常量。

# StackOverflow replaces the characters but it works in a file.
__TAB__='   ' # insert <tab> here
__LF__='
' # insert \n here
__CR__='
' #insert \r here

...或

# This one works fine when copied from here.
__TAB__="$(printf '\t')"
__LF__='
'
__CR__="$(printf '\r')"

【讨论】:

  • 您的最后一个示例不需要 IFS 技巧。命令替换不会从printf 的输出中删除制表符和回车符。
  • (事实上,在这两种情况下,您都是全局设置IFS 的值;作业本身不接受推荐作业,printf 看起来不无论如何,在其环境中为IFS 的值。)
  • @chepner 在我的系统上没有IFS 似乎工作正常。我不知道为什么我认为它会去掉“\r”,尽管我对制表符感到困惑。 IFS的默认值不是“\t\n”吗?
  • 没有对作业右侧应用分词。删除尾随换行符的是命令替换本身,与 IFS 的当前值无关。
【解决方案2】:

有更好的、符合 POSIX 的方法吗?

我不这么认为,你的方法很好,而且会非常慢。

如果“更好”是速度,您总是可以编写一个与 POSIX 兼容的 C 程序。 (但实际上,你可以编译 GNU sed 然后 sed -z 它)。

我会选择awk -v FS='' -v RS='' '{ gsub(/\\/, "\\\\"); gsub("\r", "\\r"); gsub(/\t/, "\\t"); gsub(/\n/, "\\n")} 1',甚至busybox awk 会处理它,然后用awk 编写整个内容。

你会怎么做?

printf "%b"


__LF__="
"
__TAB__=$(printf '\t')
__CR__=$(printf '\r')

【讨论】:

  • 你根本不需要printf 换行。只需在带引号的字符串中嵌入换行符即可。
  • 如果机器上有可用的编译器,我可能会写一个 C 程序,但如果要转换的数据不是太大,则可能不需要它
  • 嗯,你可以在本地交叉编译,然后复制。 TBH,如果您这样做,交叉编译busybox,然后您将拥有所有实用程序,然后将其全部写入awk。
  • 鉴于 inout 数据,我不得不编写一个 awk 程序
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-06-12
  • 1970-01-01
  • 2017-07-10
  • 2014-06-20
  • 2010-10-15
  • 1970-01-01
相关资源
最近更新 更多