我在这里阅读了一些关于在 shell 上引用变量的答案。基本上,这就是我正在运行的:
for f in "$(tmsu files)"; do echo "${f}"; done
这具有预期的结果:文件列表。不过,我很确定它们会作为单个字符串出现。经过一番阅读后,我意识到这是一个 zsh 主义;它处理分裂的方式与大多数 shell 不同。
于是我就火了bash
并运行了相同的命令。我原以为它会拆分列表,但可惜的是,我得到了确切地相同的结果。所以现在我真的很困惑。
- 为什么 bash 没有按预期运行?
- 如何让 zsh 分割线?
我已经尝试过(zsh):
for f in "$(tmsu files)"; do echo "${=f}"; done
但
for f in "$(=tmsu files)"; do echo "${f}"; done
都没有成功,所以显然我误解了如何zsh
拆分。
更糟糕的是,在某个时刻,我让它完全按照我的意愿行事,但我不记得我是如何做到的。
答案1
for f in "$(tmsu files)"; do echo "${f}"; done
这更像是错误的做法,而不是 zsh 主义。双引号告诉 shell 将扩展结果保留为单个字符串。在大多数情况下,这就是您想要的,例如,如果您有
filename="foo bar"
ls "$filename"
您希望ls
将文件名作为单个参数获取,而不是作为两个参数foo
和bar
。 (这些将是不同的文件名。)现在,您使用命令替换的事实并没有改变这一点。尽管您的观点是正确的,在这种情况下引用并不能解决您的问题,但这种构造一开始就很尴尬。 (当然,在 POSIX shell 中,即 zsh 有自己的工具)。
考虑一个文件列表,例如:
hello.txt
some silly name with spaces.txt
使用"$(output filenames)"
会给你一个字符串,而不是两个。
使用$(output filenames)
会给你六个字符串,而不是两个。
您可以通过IFS
仅设置换行符来解决这个问题,这样后者就会在换行符上拆分并为您提供两个文件名。但这很麻烦,具有全局影响,并且您仍然需要禁用通配符,以防set -f
文件名被funny * name.txt
替换。
(到目前为止,它仍然与 zsh 基本相同,因为它确实分割了未加引号的命令替换的结果,即使它没有分割变量扩展。不过,默认情况下它不会对结果进行通配。)
现在,您看不到差异的原因是echo
打印它获得的所有参数,并用单个空格连接。因此echo foo bar
并echo "foo bar"
产生完全相同的输出。相反,使用类似 的东西printf "<%s>\n" ...
,它可以让您更清楚地看到参数边界:
$ printf "<%s>\n" foo bar
<foo>
<bar>
$ printf "<%s>\n" "foo bar"
<foo bar>
也可以看看:
- 分词在格雷格的维基上
- 为什么你不读带有“for”的行
- 如何循环遍历文件的行?(它说“文件的行”,但是命令替换中是否有或其他内容并不重要
cat
),并且一如既往: - 为什么我的 shell 脚本会因为空格或其他特殊字符而卡住?
答案2
不过,我很确定它们会作为单个字符串出现。
这就是使用引号时得到的结果。我无法说出zsh
应该如何做到这一点,但bash
手册说明了这一点(“扩展”下的“分词”部分):
shell 扫描参数扩展、命令替换和算术扩展的结果没有出现在双引号内用于分词。
(强调已添加)。请注意,单引号也不会发生分词;然而,它们内部也不会发生替代和扩展。
如果您希望在命令替换中使用单独的参数,则需要删除引号。然而,如果tmsu
产生任何带有空格的结果,这些也会产生不需要的分割(因为空格和换行符都被认为是单词分隔符),使事情变得更加复杂。在 中bash
,假设tmsu
每行给出一个结果,您可以这样做:
tmsu files |
while IFS= read -r f
do
printf "<%s>\n" "$f"
done
内置read
函数读取整行输入。您可以给它任意数量的名称。除了最后一个之外的所有字符都是根据单词分隔符(默认情况下为空格)分配的,而最后一个则分配给该行的其余部分,因此这是解决bash
扩展和替换中对空格的烦人处理的好方法。
答案3
- 如何让 zsh 分割线?
使用f
参数扩展标志(详细信息见man zshexpn
。它说:在换行符处分割扩展的结果。这是 的简写ps:\n:
。)
for f in ${(f)"$( tmsu files )"}; do
print -ru2 -- $f
done
将输出发送到 stderr 以留下 stdout 来打印可管道数据(大概发生的事情不仅仅是打印输出)。
如果您的工具可以生成由 ascii 空字符(又名 NUL、\0)分隔的输出,则 zsh 可以使用标志0
( 的简写形式ps:\0:
)而不是 来分割该输出f
。
要保留空白记录,请添加@
标志和"
引号(例如"${(@f)"$( command )"}"
)。由于命令替换的性质,不可能检索尾随换行符。
如果您想在循环read
中使用while
,则类似以下内容应该在 zsh 或 bash 中工作:
while IFS= read -ru3 -d '' x || [[ $x ]]; do
{
printf >&2 '%s\n' "$x"
z=$x
} 3<&-
done 3< <(printf 'x\0y\0z')
printf 'z: %s\n' "$z"
没有字段分割或反斜杠解释,循环不在子 shell 中运行,stdin 仍然可用,并且尽管缺少尾部分隔符,但仍找到最后一个元素。