shell:在命令替换中保留尾随换行符('\ n')


14

我希望能够捕获命令替换的确切输出,包括结尾的换行符

我意识到默认情况下会删除它们,因此可能需要进行一些操作才能保留它们,并且我想保留原始的退出代码

例如,给定一个带有可变数量的尾随换行符和退出代码的命令:

f(){ for i in $(seq "$((RANDOM % 3))"); do echo; done; return $((RANDOM % 256));}
export -f f

我想运行类似:

exact_output f

并具有以下输出:

Output: $'\n\n'
Exit: 5

我对bashPOSIX和POSIX 都感兴趣sh


1
换行符是的一部分$IFS,因此不会被捕获为参数。
Deathgrip '17

4
@Deathgrip它与IFS(尝试无关( IFS=:; subst=$(printf 'x\n\n\n'); printf '%s' "$subst" )。尝试只删除换行符\tIFS
。``



Answers:


17

POSIX外壳

获得命令完整标准输出的常用技巧(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 )是:

output=$(cmd; ret=$?; echo .; exit "$ret")
ret=$?
output=${output%.}

这个想法是增加和额外.\n。命令替换只会删除 \n。你剥去.${output%.}

请注意,在以外的shell中zsh,如果输出具有NUL字节,则仍然无法使用。使用yash,如果输出不是文本,则将不起作用。

还要注意,在某些语言环境中,最后插入哪个字符很重要。.通常应该没问题,但其他一些可能不行。例如x(在某些其他答案中使用)或@在使用BIG5,GB18030或BIG5HKSCS字符集的语言环境中不起作用。在这些字符集中,许多字符的编码与或(0x78,0x40)的编码在同一字节中结束x@

例如,ū在BIG5HKSCS中为0x88 0x78(与xASCII中的0x78一样,系统上的所有字符集必须对包含英文字母@和的可移植字符集的所有字符具有相同的编码.)。所以,如果cmdprintf '\x88',我们插入x后,${output%x}将无法带,其x$output实际上包含ū

.如果有任何字符的编码以与相同的编码结尾,则在理论上使用可能会导致相同的问题.,但是对于前一段时间进行的检查,我可以断言在int的语言环境中可能没有可用的字符集。 Debian,FreeBSD或Solaris系统具有这样的字符,对我来说已经足够好了(为什么我决定在.哪个位置上也用英语来标记句子的结尾,所以似乎很合适)。

@Arrow讨论的一种更正确的方法是仅对最后一个字符(${output%.})进行剥离才能将语言环境更改为C ,这样可以确保仅剥离一个字节,但这将使代码复杂化,并可能引入兼容性问题。它自己的。

bash / zsh替代品

使用bashzsh,假设输出没有NUL,您还可以执行以下操作:

IFS= read -rd '' output < <(cmd)

要获取的退出状态cmd,您可以wait "$!"; ret=$?在中执行bash但不能在中执行zsh

rc / es / akanaga

为了完整起见,注意rc/ es/ akanga有操作员。在它们中,命令替换表示为`cmd(或`{cmd}对于更复杂的命令)返回一个列表($ifs默认情况下,通过在space-tab-newline 上进行分割)。在这些shell中(与类似Bourne的shell相对),换行符的剥离仅是该$ifs拆分的一部分。因此,您可以为空$ifs或使用在``(seps){cmd}其中指定分隔符的表单:

ifs = ''; output = `cmd

要么:

output = ``()cmd

在任何情况下,命令的退出状态都会丢失。您需要将其嵌入到输出中,然后将其提取出来,这将变得很丑陋。

在fish中,命令替换带有(cmd)且不涉及子外壳。

set var (cmd)

创建一个$var数组,其中cmdif 输出中的所有行都$IFS是非空的,或者如果输出为空,则输出的输出cmd最多剥离一个(与大多数其他shell中的所有输出相反)换行符$IFS

所以还是有一个问题,在(printf 'a\nb')(printf 'a\nb\n')扩展到同样的事情,即使空$IFS

要解决此问题,我能想到的最好的方法是:

function exact_output
  set -l IFS . # non-empty IFS
  set -l ret
  set -l lines (
    cmd
    set ret $status
    echo
  )
  set -g output ''
  set -l line
  test (count $lines) -le 1; or for line in $lines[1..-2]
    set output $output$line\n
  end
  set output $output$lines[-1]
  return $ret
end

一种替代方法是:

read -z output < (begin; cmd; set ret $status; end | psub)

伯恩壳

Bourne Shell不支持该$(...)表单,也不支持该${var%pattern}运算符,因此要在此实现可能会非常困难。一种方法是使用eval和引用:

eval "
  output='`
    exec 4>&1
    ret=\`
      exec 3>&1 >&4 4>&-
      (cmd 3>&-; echo \"\$?\" >&3; printf \"'\") |
        awk 3>&- -v RS=\\\\' -v ORS= -v b='\\\\\\\\' '
          NR > 1 {print RS b RS RS}; {print}; END {print RS}'
    \`
    echo \";ret=\$ret\"
  `"

在这里,我们正在生成一个

output='output of cmd
with the single quotes escaped as '\''
';ret=X

被传递给eval。至于POSIX方法,如果'其中一个字符可以在其他字符的末尾找到其编码,那么我们会遇到一个问题(一个更糟糕的问题,因为它将成为命令注入漏洞),但值得庆幸的是,例如.,它不是其中的一种,而且引号技术通常是所有用于引号shell代码的技术(请注意\存在问题,因此不应该使用引号技术(还排除了"..."在其中需要对某些字符使用反斜杠的情况))在这里,我们仅在a '可以使用之后才能使用它)。

tcsh

参见tcsh在命令替换`...`中保留换行符。

(不考虑退出状态,可以通过将其保存在临时文件中(echo $status > $tempfile:q在命令之后)来解决此问题)


谢谢-特别是关于不同字符集的线索。如果zsh可以存储NUL在变量中,为什么IFS= read -rd '' output < <(cmd)不起作用?它需要能够存储一个字符串的长度...它编码''为1字节的字符串\0而不是0字节的字符串吗?
汤姆·黑尔

1
@TomHale,是的,read -d ''被视为read -d $'\0'bash尽管$'\0'与其他''地方一样)。
斯特凡Chazelas

您正在混合字符和字节。请理解,如果我们完全删除添加的内容,则原始实体不得更改。这并不是说难治删除一个字节称为x如果是加入什么。请看看我编辑后的答案。
艾萨克(Isaac)

@Arrow,是的,var=value command eval技巧已经在此处)和之前的austin-group邮件列表中进行了讨论。您会发现它不是可移植的(当您尝试类似a=1 command eval 'unset a; a=2'或更糟糕的事情时,它不应该那样使用是很明显的)。同样的savedVAR=$VAR;...;VAR=$savedVAR原因$VAR是,最初设置时它不执行您想要的操作。如果这只是为了解决一个理论问题(在实践中无法发现的错误),IMO,那是不值得的。不过,我会支持您的尝试。
斯特凡Chazelas

您是否有链接到您讨论过的地方,最后放弃了使用LANG=C来从字符串中删除字节?您正在提出关于实际问题的关注,所有这些都很容易解决。(1)没有未使用的设置(2)更改变量之前先对其进行测试。@StéphaneChazelas–
艾萨克(Isaac)

3

对于新问题,此脚本有效:

#!/bin/bash

f()           { for i in $(seq "$((RANDOM % 3 ))"); do
                    echo;
                done; return $((RANDOM % 256));
              }

exact_output(){ out=$( $1; ret=$?; echo x; exit "$ret" );
                unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
                LC_ALL=C ; out=${out%x};
                unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL
                 printf 'Output:%10q\nExit :%2s\n' "${out}" "$?"
               }

exact_output f
echo Done

执行时:

Output:$'\n\n\n'
Exit :25
Done

较长的描述

POSIX shell处理删除的通常的智慧\n是:

添加一个 x

s=$(printf "%s" "${1}x"); s=${s%?}

这是必需的,因为根据POSIX规范,命令扩展会删除最后一个新行(S):

在替换结束时删除一个或多个字符的序列。


关于尾随x

在这个问题中有人说过,“ an” x可以与某种编码中某个字符的尾随字节混淆。但是,我们将如何猜测以某种可能的编码在某种语言中哪个或哪个字符更好,这至少可以说是一个困难的命题。

然而; 那简直是不对的

我们需要遵循的唯一规则是准确添加要删除的内容。

容易理解,如果我们在现有字符串(或字节序列)中添加内容,然后又删除完全相同的内容,则原始字符串(或字节序列)必须相同。

我们哪里出错了?当我们混合 字符字节时

如果添加一个字节,则必须删除一个字节,如果添加一个字符,则必须删除完全相同的字符

第二种选择是添加一个字符(并随后删除完全相同的字符),这可能会令人费解和复杂,并且可以,代码页和编码可能会受到干扰。

但是,第一种选择是完全有可能的,并且在解释了它之后,它将变得很简单。

让我们添加一个字节,一个ASCII字节(<127),并尽可能减少混乱,让我们假设az范围内的ASCII字符。或者,正如我们应该说的那样,十六进制范围0x61- 中的一个字节0x7a。让我们选择其中的任何一个,也许是x(实际上是value的一个字节0x78)。我们可以通过将x连接到字符串来添加这样的字节(假设为é):

$ a
$ b=${a}x

如果将字符串视为字节序列,则会看到:

$ printf '%s' "$b" | od -vAn -tx1c
  c3  a9  78
 303 251   x

以x结尾的字符串序列。

如果删除该x(字节值0x78),则会得到:

$ printf '%s' "${b%x}" | od -vAn -tx1c
  c3  a9
 303 251

它可以正常工作。

一个更困难的例子。

假设我们感兴趣的字符串以byte结尾0xc3

$ a=$'\x61\x20\x74\x65\x73\x74\x20\x73\x74\x72\x69\x6e\x67\x20\xc3'

并添加一个值字节 0xa9

$ b=$a$'\xa9'

字符串现在变成了这个:

$ echo "$b"
a test string é

正是我想要的,后两个字节是utf8 中的一个字符(因此任何人都可以在其utf8控制台中重现此结果)。

如果我们删除一个字符,则原始字符串将被更改。但这不是我们添加的内容,而是添加了一个字节值,该值恰好被写为x,但无论如何还是一个字节。

我们需要避免将字节误解为字符。我们需要一个删除我们使用的字节的动作0xa9。实际上,ash,bash,lksh和mksh似乎都可以做到这一点:

$ c=$'\xa9'
$ echo ${b%$c} | od -vAn -tx1c
 61  20  74  65  73  74  20  73  74  72  69  6e  67  20  c3  0a
  a       t   e   s   t       s   t   r   i   n   g     303  \n

但不是ksh或zsh。

但是,这很容易解决,让我们告诉所有这些shell进行字节删除:

$ LC_ALL=C; echo ${b%$c} | od -vAn -tx1c 

就是这样,所有的shell测试工作(yash除外)(对于字符串的最后一部分):

ash             :    s   t   r   i   n   g     303  \n
dash            :    s   t   r   i   n   g     303  \n
zsh/sh          :    s   t   r   i   n   g     303  \n
b203sh          :    s   t   r   i   n   g     303  \n
b204sh          :    s   t   r   i   n   g     303  \n
b205sh          :    s   t   r   i   n   g     303  \n
b30sh           :    s   t   r   i   n   g     303  \n
b32sh           :    s   t   r   i   n   g     303  \n
b41sh           :    s   t   r   i   n   g     303  \n
b42sh           :    s   t   r   i   n   g     303  \n
b43sh           :    s   t   r   i   n   g     303  \n
b44sh           :    s   t   r   i   n   g     303  \n
lksh            :    s   t   r   i   n   g     303  \n
mksh            :    s   t   r   i   n   g     303  \n
ksh93           :    s   t   r   i   n   g     303  \n
attsh           :    s   t   r   i   n   g     303  \n
zsh/ksh         :    s   t   r   i   n   g     303  \n
zsh             :    s   t   r   i   n   g     303  \n

就是这么简单,告诉外壳删除LC_ALL = C字符,对于从0x00to的所有字节值,该字符正好是一个字节0xff

评论解决方案:

对于注释中讨论的示例,一种可能的解决方案(在zsh中失败)是:

#!/bin/bash

LC_ALL=zh_HK.big5hkscs

a=$(printf '\210\170');
b=$(printf '\170');

unset OldLC_ALL ; [ "${LC_ALL+set}" ] && OldLC_ALL=$LC_ALL
LC_ALL=C ; a=${a%"$b"};
unset LC_ALL ; [ "${OldLC_ALL+set}" ] && LC_ALL=$OldLC_ALL

printf '%s' "$a" | od -vAn -c

这将消除编码问题。


很高兴知道可以删除多个尾随换行符。
汤姆·黑尔


我同意,将语言环境固定为C以确保${var%?}始终剥离一个字节在理论上是更正确的,但是:1- LC_ALLLC_CTYPEoverride $LANG,因此您需要设置LC_ALL=C2-不能var=${var%?}在子shell中进行操作,因为更改会丢失,因此您需要保存和恢复LC_ALL(或诉诸非POSIX local范围功能)的值和状态。3-在某些外壳程序(如yash)中不完全支持在脚本中途更改语言环境。另一方面,在实践.中,在现实生活中的字符集从来都不是问题,因此使用它可以避免与LC_ALL混合。
斯特凡Chazelas

2

您可以在正常输出之后输出一个字符,然后将其剥离:

#capture the output of "$@" (arguments run as a command)
#into the exact_output` variable
exact_output() 
{
    exact_output=$( "$@" && printf X ) && 
    exact_output=${exact_output%X}
}

这是POSIX兼容的解决方案。


根据答复,我认为我的问题不清楚。我刚刚更新了它。
汤姆·黑尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.