我希望能够捕获命令替换的确切输出,包括尾随的换行符。
我意识到它们默认被剥离,因此可能需要一些操作才能保留它们,并且我想保留原来的退出代码。
例如,给定一个带有可变数量的尾随换行符和退出代码的命令:
f(){ for i in $(seq "$((RANDOM % 3))"); do echo; done; return $((RANDOM % 256));}
export -f f
我想运行类似的东西:
exact_output f
并让输出为:
Output: $'\n\n'
Exit: 5
bash
我对POSIX 和 POSIX都感兴趣sh
。
答案1
POSIX shell
通常 (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 字符集的区域设置中工作。在这些字符集中,许多字符的编码结束x
与or @
(0x78, 0x40)的编码在同一字节中
例如,ū
在 BIG5HKSCS 中为 0x88 0x78(与x
ASCII 中的 0x78 类似,系统上的所有字符集对于可移植字符集的所有字符必须具有相同的编码,其中包括英文字母@
和.
)。因此,如果cmd
是printf '\x88'
(它本身不是该编码中的有效字符,而只是一个字节序列)并且我们x
在它后面插入,${output%x}
则无法将其剥离x
为$output
实际包含的内容ū
(组成字节序列的两个字节是该编码中的有效字符)。
使用.
或/
应该是总体来说还好,按照 POSIX 的要求:
<period>
“与、<slash>
、<newline>
和关联的编码值<carriage-return>
在实现支持的所有语言环境中应保持不变。”,这意味着这些值在任何语言环境/编码中都将具有相同的二进制表示形式。- “同样,用于编码
<period>
、<slash>
、<newline>
和的字节值<carriage-return>
不得作为任何语言环境中任何其他字符的一部分出现。”,这意味着上述情况不会发生,因为这些字节/字符无法完成部分字节序列任何语言环境/编码中的有效字符。 (看6.1 可移植字符集)
上述内容不适用于可移植字符集的其他字符。
另一种方法,如由@Isaac 讨论,会将语言环境更改为C
(这也将保证任何单个字节可以正确剥离),仅用于剥离最后一个字符(${output%.}
)。通常有必要使用LC_ALL
它(原则上LC_CTYPE
就足够了,但这可能会被任何已设置的意外覆盖LC_ALL
)。此外,还需要恢复原始值(或者例如locale
在函数中使用不符合 POSIX 标准的值)。但请注意,某些 shell 不支持在运行时更改区域设置(尽管 POSIX 要求这样做)。
通过使用.
或/
,所有这些都可以避免。
bash/zsh 替代品
使用bash
和zsh
,假设输出没有 NUL,您还可以执行以下操作:
IFS= read -rd '' output < <(cmd)
要获取 的退出状态,您可以在 的某些版本中cmd
执行,但不能在中执行,但您可以编写它并在 中获取退出状态。wait "$!"; ret=$?
bash
zsh
zsh
cmd | IFS= read -rd '' output
$pipestatus[1]
rc/es/akanaga
为了完整起见,请注意rc
//es
有akanga
一个运算符。在它们中,命令替换,表示为`cmd
(或`{cmd}
对于更复杂的命令)返回一个列表(默认情况下通过分割$ifs
,空格制表符换行符)。在这些 shell 中(与类似 Bourne 的 shell 不同),换行符的剥离仅作为$ifs
拆分的一部分进行。因此,您可以清空$ifs
或使用``(seps){cmd}
指定分隔符的表单:
ifs = ''; output = `cmd
或者:
output = ``()cmd
无论如何,命令的退出状态都会丢失。您需要将其嵌入到输出中,然后再提取它,这会变得很难看。
鱼
在fish中,命令替换是带有子shell的(cmd)
,并且不涉及子shell。
set var (cmd)
创建一个数组,其中包含if$var
的输出中的所有行,或者删除最多的输出cmd
$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
从版本 3.4.0(2022 年 3 月发布)开始,您可以改为:
set output (cmd | string collect --allow-empty --no-trim-newlines)
对于旧版本,您可以执行以下操作:
read -z output < (begin; cmd; set ret $status; end | psub)
需要注意的是,$output
如果没有输出,则这是一个空列表,而不是带有一个空元素的列表。
版本 3.4.0 还添加了对$(...)
其行为的支持,(...)
除了它也可以在双引号内使用,在这种情况下,它的行为类似于 POSIX shell:输出不会按行拆分,但所有尾随换行符都会被删除。
伯恩外壳
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
(不关心退出状态,您可以通过将其保存在临时文件中(echo $status > $tempfile:q
在命令之后)来解决该问题)
答案2
对于新问题,此脚本有效:
#!/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%?}
这是必需的,因为最后一个新行(S)通过命令扩展删除POSIX规范:
在替换结束时删除一个或多个字符的序列。
关于尾随x
.
在这个问题中有人说过, anx
可能与某些编码中某些字符的尾随字节混淆。但是,我们如何猜测某种语言中某种可能的编码中哪个或哪个字符更好,至少可以说,这是一个困难的命题。
然而;那简直就是不正确。
我们需要遵循的唯一规则是添加确切地我们删除的内容。
应该很容易理解,如果我们向现有字符串(或字节序列)添加一些内容,然后删除确切地相同的东西,原始字符串(或字节序列)必须是相同的。
我们哪里出了问题?什么时候我们混合 人物和字节。
如果我们添加一个字节,我们必须删除一个字节,如果我们添加一个字符,我们必须删除完全相同的角色。
第二个选项,添加一个字符(然后删除完全相同的字符)可能会变得令人费解和复杂,并且,是的,代码页和编码可能会妨碍。
然而,第一个选项是很有可能的,并且在解释它之后,它就会变得非常简单。
让我们添加一个字节,一个 ASCII 字节 (<127),并尽可能减少复杂性,假设在 az 范围内有一个 ASCII 字符。或者正如我们应该说的,十六进制范围内的一个字节0x61
- 0x7a
。让我们选择其中任何一个,也许是一个x(实际上是一个字节的值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
就这么简单,告诉 shell 删除 LC_ALL=C 字符,该字符恰好是从 到0x00
的所有字节值的一个字节0xff
。
请注意,某些 shell 不支持在运行时更改区域设置(尽管 POSIX 要求这样做)。
通常无需更改区域设置即可工作的解决方案
虽然上面的代码应该适用于任何(除了换行符或空)字节作为哨兵值,但它可以变得更容易,而无需更改区域设置:
使用.
或/
应该是总体来说还好,按照 POSIX 的要求:
<period>
“与、<slash>
、<newline>
和关联的编码值<carriage-return>
在实现支持的所有语言环境中应保持不变。”,这意味着这些值在任何语言环境/编码中都将具有相同的二进制表示形式。- “同样,用于编码
<period>
、<slash>
、<newline>
和的字节值<carriage-return>
不得作为任何语言环境中任何其他字符的一部分出现。”,这意味着上述情况不会发生,因为这些字节/字符无法完成部分字节序列任何语言环境/编码中的有效字符。 (看6.1 可移植字符集)
上述内容不适用于可移植字符集的其他字符。
评论解决方案:
对于评论中讨论的示例,一种可能的解决方案(在 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
这将消除编码问题。
答案3
您可以在正常输出后输出一个字符,然后将其剥离:
#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 标准的解决方案。
答案4
这是一个 bash 函数,封装了 @Isaac 描述的 LC_ALL=C 技术。
# This function provides a general solution to the problem of preserving
# trailing newlines in a command substitution.
#
# cmdsub <command goes here>
#
# If the command succeeded, the result will be found in variable CMDSUB_RESULT.
cmdsub() {
local -r BYTE=$'\x78'
local result
if result=$("$@"; ret=$?; echo "$BYTE"; exit "$ret"); then
local LC_ALL=C
CMDSUB_RESULT=${result%"$BYTE"}
else
return "$?"
fi
}
笔记:
$'\x78'
选择虚拟字节是为了测试本问答讨论中讨论的极端情况,但可以使用除换行符 (0x0A
) 和 NUL (0x00
) 之外的任何字节。- 将其封装在函数中还有一个额外的好处,即我们可以将 LC_ALL 设为局部变量,从而避免保存和恢复其值的需要。
- 我考虑使用 bash 4.3 的 nameref 功能来允许调用者提供应存储结果的变量的名称,但我决定最好支持旧版 bash。
- 原则上设置,
LC_CTYPE
应该足够了,但是如果LC_ALL
已经设置了“外部”,则将覆盖前者。
使用 bash 4.1 成功测试了 BIG5HKSCS 极端情况:
#!/bin/bash
LC_ALL=zh_HK.big5hkscs
cmdsub() {
local -r BYTE=$'\x78'
local result
if result=$("$@"; ret=$?; echo "$BYTE"; exit "$ret"); then
local LC_ALL=C
CMDSUB_RESULT=${result%"$BYTE"}
else
return "$?"
fi
}
cmd() { echo -n $'\x88'; }
if cmdsub cmd; then
v=$CMDSUB_RESULT
printf '%s' "$v" | od -An -tx1
else
printf "The command substitution had a non-zero status code of %s\n" "$?"
fi
结果88
正如预期的那样。