【问题标题】:Why does zsh expand globs for me in a bash script using GNU parallel?为什么 zsh 在使用 GNU 并行的 bash 脚本中为我扩展 glob?
【发布时间】:2018-05-23 13:48:11
【问题描述】:

在 bash 脚本中,我有一个使用 rsync 的命令:

#!/usr/bin/bash -e
...
parallel rsync --exclude '*to?be?deleted*' ... 
    --files-from some_file /auto $instance_ip:/somewhere_else/

根据 rsync 的文档,他们的 --exclude 字段具有不同风格的模式匹配

当我在 bash 终端中运行它时,它运行良好。

但是,在 zsh 上运行它会给我一个错误,因为 zsh 试图扩展我试图传入的这个文字字符串:

zsh:1: no matches found: *to?be?deleted*

这应该发生。为什么 zsh 甚至首先在我的 bash 脚本中扩展我的 glob?我的 zsh 上是否有一些设置可以设置以使两者的行为方式相同?我不想在 zsh 中开发并使用 bash 部署到环境中,并且必须以不同的方式行事。

我正在使用 oh-my-zsh 的插件:

plugins=(
  git
  colored-man-pages
  zsh-autosuggestions
  zsh-syntax-highlighting
)

具体来说,使用这组命令会失败:

#!/usr/bin/bash -e
find . -name '*filelist' | parallel -j10 rsync --exclude "*to?be?deleted*" testing somewhere_else:/some/where/else

但是单独使用 rsync 的命令,它不会中断。

【问题讨论】:

  • 你是如何运行脚本的?
  • $ bash my_script.sh
  • 那就特别出人意料了。你的脚本中的... 是什么,没有它你会得到同样的东西吗?
  • 好吧,...相当长,但它正在设置云实例并进行一些预处理。我正在尝试rsync 处理这些文件。我尝试做一个简单的rsync 并且成功了。我已经在我的问题中发送了一个 MVCE :)
  • 您在 bash 脚本中引用了该模式,但 parallel 的所有额外参数都传递给 另一个 shell,减去了 bash 已经删除的引号。

标签: bash zsh gnu-parallel


【解决方案1】:

问题在于 GNU Parallel 实用程序。尽管看起来您正在向它传递一个程序以使用参数运行,但它实际上所做的是将参数连接起来并将它们传递给 shell。

此外,Parallel 要么运行与 parallel 相同的 shell,要么根据 SHELL 环境变量选择 shell(这是有问题的,因为终端仿真器也使用此环境变量来决定使用哪个交互式 shell跑)。无论哪种方式,这都是它选择 zsh 而不是 sh 的原因。与 sh(bash、dash、ksh、...)兼容的 shell 也会有同样的问题,但更罕见的是:如果模式不匹配任何内容,sh 会单独留下模式,因此只要使用 sh,您的脚本就可以工作当前目录中没有匹配*to?be?deleted*的文件。

解决方案在手册中给出,但有点难找:通过-q 选项。该手册有很长的一章关于引用,您可以在 99% 的情况下忽略它:只需传递 -q,除非您打算运行 shell 脚本 而不是 命令 .此外,您应该使用命令的完整路径,否则并行可能会调用内置的 shell 甚至函数(如果您的 shell 是 bash)。另外,将SHELL 设置为/bin/sh,因为即使使用-q,Parallel 也会运行一个shell,并假设它与sh 兼容(我认为zsh 足够兼容,但我不完全确定。)。另见a similar question on Unix Stack Exchange

SHELL=/bin/sh parallel -q -j10 "$(command -v rsync)" --exclude "*to?be?deleted*" testing somewhere_else:/some/where/else

(是的,手册不鼓励您使用-q,但这是错误的。我之前曾为此与作者争论过。)

【讨论】:

  • 这是一个非常全面的答案。谢谢 :) 我敢打赌,无数使用与 bash 不同的 shell 的人都遇到过这种情况,并且想知道为什么他们的 bash 脚本会调用 zsh。
  • 根据其描述,甚至不清楚-q 做了什么。它是否在每个单词周围添加一层引号,例如'rsync' '--exclude' '*to?be?deleted*' 'testing' 'somewhere...'
  • @chepner 好点,它传递一个外壳并引用字符串。 Stéphane Chazelas tried to explain to the author how shells work, but in vain.
  • @chepner -q 引用命令和参数。引用的实际方式取决于 shell,但对于大多数 shell,它是 \-quoted。 -vv 将显示它实际运行的内容。
【解决方案2】:

parallel 正在使用一个由它传递的参数组成的字符串来启动你的登录 shell 实例。您的 bash 脚本在传递参数之前去掉引号,因此并行执行相当于

zsh -c "rsync --exclude *to?be?deleted* testing somewhere_else:/some/where/else"

其中模式被引用。为防止这种情况,请将单个字符串作为参数传递给parallel

... | parallel -j10 'rsync --exclude "*to?be?deleted*" testing somewhere_else:/some/where/else'

【讨论】:

  • 我不确定将parallel 将参数连接在一起形成一个字符串,然后将该字符串传递给shell 是否公平。当 任何人 这样做时,这是不好的做法,即使这样做太频繁(如 ssh 之类的人)。
  • 我不是在责怪parallel;我看不出它还能做什么。我只是指出这就是zsh 试图扩展被认为是引用模式的原因。
  • (或者我错过了删除的评论?)
  • 我明白了。 parallel 这样做非常危险,我已将其更改为完整的报价。谢谢:)
  • 让我成为一个纯粹主义者——我的观点是 ssh(或 parallel 或任何其他程序)隐式运行 "${SHELL:-sh}" -c 是明显错误的,并且给定 argv 的程序用于调用另一个软件应该将它直接传递给一个 execv 风格的调用并且不加修改,而不是尝试从中构建一个字符串。因此,在那些并行向用户保证要求调用shell而不将sh作为参数传递的地方,我的立场是,它是非常糟糕的设计。
【解决方案3】:

GNU 并行版本 $SHELL。以后的版本会尝试检测 GNU Parallel 从哪个 shell 启动,并改用那个 shell。有关检测的详细信息,请参阅man parallel_design (Which shell to use)。这里还解释了为什么 GNU Parallel 总是在 shell (Always running commands in a shell) 中运行命令。

如果你不想让 shell 扩展特殊字符,你可以使用-q。 但是,该命令必须是一个简单的命令(参见man bash),没有重定向和变量分配。这个 将引用命令行和参数,因此特殊字符是 不被 shell 解释。

【讨论】:

  • 谢谢你。我感谢调查。由于组织政策,我不一定会更新这台机器上的 gnu 并行,但会注明以备将来使用。
  • 如果允许您运行自己的脚本,那么您可以使用--embed (>=20180322) 或进行个人安装。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-02-01
  • 2020-06-27
  • 1970-01-01
  • 1970-01-01
  • 2011-01-03
  • 1970-01-01
  • 2022-06-11
相关资源
最近更新 更多