我不太理解 shell 上的扩展和引用(zsh/bash)

我不太理解 shell 上的扩展和引用(zsh/bash)

我在这里阅读了一些关于在 shell 上引用变量的答案。基本上,这就是我正在运行的:

for f in "$(tmsu files)"; do echo "${f}"; done

这具有预期的结果:文件列表。不过,我很确定它们会作为单个字符串出现。经过一番阅读后,我意识到这是一个 zsh 主义;它处理分裂的方式与大多数 shell 不同。

于是我就火了bash并运行了相同的命令。我原以为它会拆分列表,但可惜的是,我得到了确切地相同的结果。所以现在我真的很困惑。

  1. 为什么 bash 没有按预期运行?
  2. 如何让 zsh 分割线?

我已经尝试过(zsh): for f in "$(tmsu files)"; do echo "${=f}"; donefor 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将文件名作为单个参数获取,而不是作为两个参数foobar。 (这些将是不同的文件名。)现在,您使用命令替换的事实并没有改变这一点。尽管您的观点是正确的,在这种情况下引用并不能解决您的问题,但这种构造一开始就很尴尬。 (当然,在 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 barecho "foo bar"产生完全相同的输出。相反,使用类似 的东西printf "<%s>\n" ...,它可以让您更清楚地看到参数边界:

$ printf "<%s>\n" foo bar
<foo>
<bar>
$ printf "<%s>\n" "foo bar"
<foo bar>

也可以看看:

答案2

不过,我很确定它们会作为单个字符串出现。

这就是使用引号时得到的结果。我无法说出zsh应该如何做到这一点,但bash手册说明了这一点(“扩展”下的“分词”部分):

shell 扫描参数扩展、命令替换和算术扩展的结果没有出现在双引号内用于分词。

(强调已添加)。请注意,单引号也不会发生分词;然而,它们内部也不会发生替代和扩展。

如果您希望在命令替换中使用单独的参数,则需要删除引号。然而,如果tmsu产生任何带有空格的结果,这些也会产生不需要的分割(因为空格和换行符都被认为是单词分隔符),使事情变得更加复杂。在 中bash,假设tmsu每行给出一个结果,您可以这样做:

tmsu files |
    while IFS= read -r f
    do
        printf "<%s>\n" "$f"
    done

内置read函数读取整行输入。您可以给它任意数量的名称。除了最后一个之外的所有字符都是根据单词分隔符(默认情况下为空格)分配的,而最后一个则分配给该行的其余部分,因此这是解决bash扩展和替换中对空格的烦人处理的好方法。

答案3

  1. 如何让 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 仍然可用,并且尽管缺少尾部分隔符,但仍找到最后一个元素。

相关内容