使用POSIX工具将换行符替换为字符串“\n”

3
我知道有许多类似的问题,例如(0)(1),但我认为没有一个真正回答我想要的内容。
我想要的是将任何换行符(LF)替换为字符串\n不会隐含任何换行符,且要使用仅基于POSIX的工具(无GNU扩展或Bashisms),并且从stdin中读取输入不希望进行缓冲
例如:
  • printf 'foo' | magic 应该得到 foo
  • printf 'foo\n' | magic 应该得到 foo\n
  • printf 'foo\n\n' | magic 应该得到 foo\n\n
通常给出的答案不能做到这一点,例如:
  • awk
    printf 'foo' | awk 1 ORS='\\n 得到foo\n,而实际上应该只得到foo
    因此,在没有换行符的情况下添加了一个\n
  • sed
    对于仅有一个foo的情况可以工作,但在所有其他情况下,例如:
    printf 'foo\n' | sed ':a;N;$!ba;s/\n/\\n/g' 得到foo,而实际上应该得到foo\n
    缺少一个最终的换行符。
    由于我不想进行任何形式的缓冲,也不能仅查看输入是否以换行符结尾,然后手动添加缺少的部分。
    无论如何...它都将使用GNU扩展。
    sed -z 's/\n/\\n/g'
    确实可以工作(甚至正确地保留NULs),但是再次,这是GNU扩展。
  • tr
    只能替换为一个字符,而我需要两个字符。
迄今为止我唯一找到的可行方案是使用perl:
perl -p -e 's/\n/\\n/'
完全符合期望的结果,但正如我所说,我希望在仅有基本POSIX工具的环境下解决此问题(因此没有Perl或使用任何GNU扩展)。
提前感谢。

printf 'foo\\n' 应该输出什么? - αғsнιη
你面临的大问题是 printf 'foo' 的输出不是一个有效的 POSIX 文本文件(它缺少必需的终止换行符),因此任何 POSIX 文本处理工具在处理该输入时的行为都是未定义的。这意味着,如果你使用某个实现(例如 GNU)的 sed、awk 或其他工具来解决问题,即使在 POSIX 兼容模式下运行,也不能保证相同的解决方案适用于该工具的任何其他版本(例如 BSD),因为任何工具都可以对处理方式未定义的输入执行任何操作,并仍然符合 POSIX 标准。 - Ed Morton
除了@rowboat上面提到的关于POSIX文本文件是零个或多个的参考资料之外,POSIX定义的一行一个由零个或多个非<newline>字符组成的序列加上一个终止的<newline>字符。请注意,对于POSIX文本行,需要一个终止的<newline>字符,因此POSIX文本文件中的每一行都必须以<newline>字符结尾,如果您输入到文本处理实用程序(例如sed或awk)的内容不是这样,则可能会有所不同。 - Ed Morton
@rowboat 好的,那很清楚了,这并不是一个要求,只是我说GNU的sed和-z选项可以很好地处理NUL。 - calestyo
@αғsнιη printf 'foo\n' - 在这里打印将会看到字符 f o o \ \ n ... 它将会将双反斜杠解释为文字上的一个反斜杠。因此,实际打印的字符串是 'foo\n' ... 所有字符都是文字意义上的。由于没有换行符,输出应该再次是 foo\n - calestyo
3个回答

2
以下内容适用于所有使用POSIX版本工具的情况,以及任何POSIX文本允许的字符作为输入,无论是否存在终止换行符:
$ magic() { { cat -u; printf '\n'; } | awk -v ORS= '{print sep $0; sep="\\n"}'; }

$ printf 'foo' | magic
foo$

$ printf 'foo\n' | magic
foo\n$

$ printf 'foo\n\n' | magic
foo\n\n$

该函数首先向传入的管道数据添加一个换行符,以确保awk读取的是有效的POSIX文本文件(必须以换行符结尾),因此可以保证在所有符合POSIX标准的awk中正常工作。然后awk命令会丢弃我们添加的终止换行符并将所有其他换行符替换为所需的"\n"
上述实用程序中唯一需要处理没有终止换行符的输入的是cat,但是POSIX只讨论“文件”作为cat的输入,而不是像awksed规范中的“文本文件”,因此每个符合POSIX标准的cat版本都可以处理没有终止换行符的输入。

2

我认为你可以用纯POSIX shell实现这个功能。我假设你处理的是文本,而不是任意二进制数据,其中可能包含空字节。

magic () {
  while read x; do
      printf '%s\\n' "$x"
  done
  printf '%s' "$x"
}

read 假定 POSIX 文本行(以换行符结尾),但如果没有看到换行符,它仍然会读取任何内容并将其填充到 x 中,直到输入结束。因此,只要 read 成功,你就可以在 x 中拥有一个正确的行(减去换行符),你可以写回该行,但是应该用文字字面量 \n 代替换行符。

一旦循环中断,在失败的 read 后输出 x 中的任何内容(如果有),但不要包含尾随的文字字面量 \n

$ [ "$(printf foo | magic)" = foo ] && echo passed
passed
$ [ "$(printf 'foo\n' | magic)" = 'foo\n' ] && echo passed
passed
$ [ "$(printf 'foo\n\n' | magic)" = 'foo\n\n' ] && echo passed
passed

这也是一个相当不错的解决方案。我没想到read在到达\n之前到达EOF时具有>0的退出状态。遗憾的是,它还将>0用于任何其他错误,并且不区分两者。仍然感谢您提供的解决方案。 - calestyo
我刚仔细看了一下,你的解决方案在某些情况下会失败。如果不使用-r选项读取,则\会被特殊处理,因此printf'%s''\t'| magic现在只会返回t。 - calestyo
此外,read(1) 还会进行字段拆分操作,如果变量数少于字段数,则将它们全部打包到最后一个(这里是唯一的)变量中,但同时也会丢弃任何尾随的 IFS 字符。不过,可以通过使用 IFS='' read -r x 来解决这个问题。 - calestyo

1
这里有一个 tr + sed 的解决方案,适用于任何 POSIX shell,因为它不调用任何 GNU 实用程序。
printf 'foo' | tr '\n' '\7' | sed 's/\x7/\\n/g'
foo

printf 'foo\n' | tr '\n' '\7' | sed 's/\x7/\\n/g'
foo\n

printf 'foo\n\n' | tr '\n' '\7' | sed 's/\x7/\\n/g'
foo\n\n

细节:

  • tr 命令用 \x07 替换每个换行符
  • sed 命令用 \\n 替换每个 \x07

1
如果输入已经包含\7(或任何其他字符),那么这将失败。 - Ed Morton
虽然在文本文件中出现\x07(或任何其他控制字符)的实际可能性相当低,但这是可以接受的。 - anubhava
1
是的,我知道,但没有必要例外。另一个值得注意的事情是,在 tr 之后,输出不再是有效的 POSIX 文本文件,因为它缺少终止换行符,所以您可能需要考虑使用任何给定的 sed 命令。您使用的 sed 命令无法按照您的预期进行操作的概率也相当低。 - Ed Morton
@EdMorton 很不幸的是,sed 实现似乎有很大的差异,即使 POSIX 也无法完全定义所有内容(请参见我的 https://www.austingroupbugs.net/view.php?id=1551)...但我从未见过任何无法处理末尾没有换行符的输入的实现。您知道任何这样的实现吗? - calestyo
据我所知,我从未使用sed读取没有终止换行符的输入,因此我不知道我使用过的任何sed是否适用于该输入。抱歉。 - Ed Morton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接