【问题标题】:Running commands in subdirectories with bash使用 bash 在子目录中运行命令
【发布时间】:2016-07-12 19:36:17
【问题描述】:

我有一系列目录,我需要在这些目录上运行各种 shell 命令,并且我制作了一个名为 dodirs.sh 的简短脚本来简化在每个目录中运行命令:

#!/bin/bash
echo "Running in each directory: $@"
for d in ./*/; do
    (
    cd "$d"
    pwd
    eval "$@"
)
done

这对于很多简单的命令来说是可以的,但是有些就麻烦了,比如:

grep "free  energy   TOTEN" OUTCAR | tail -1

在位于每个目录的文件中查找字符串。

似乎管道和/或引号是麻烦,因为如果我说:

dodirs.sh grep "free  energy   TOTEN" OUTCAR

我得到了一个明智的(如果 waaaay 到长输出):

Running in each directory: grep free  energy   TOTEN OUTCAR
...
OUTCAR:  free energy    TOTEN  =      -888.53122906 eV
OUTCAR:  free energy    TOTEN  =      -888.53132396 eV
OUTCAR:  free  energy   TOTEN  =      -888.531324 eV
...

我注意到 echo 的结果丢失了引号,所以这有点奇怪。另一方面,如果我说:

dodirs.sh grep "free  energy   TOTEN" OUTCAR | tail -1

然后我得到了无意义的:

...
grep: energy: No such file or directory
grep: TOTEN: No such file or directory
...

请注意,回声现在根本没有回声,显然是误解了这条线。

有什么方法可以转义字符,或者将参数打包到我的 dodirs.sh 脚本中吗?

也许有人完全知道更好的方法?

【问题讨论】:

  • eval "$@" 有点……违反直觉。一般来说,您应该采用文字 argv 数组,在这种情况下您只需使用 "$@" 来执行它,或者采用单个字符串,在这种情况下您应该使用 eval "$string"
  • ...eval "$string" 方法相对于eval "$@" 的最大优势在于调用者必须知道它正在使用中才能成功使用它——他们可以'不要认为他们正在调用采用execv 方法的东西,而是调用使用eval 方法的东西,这是安全漏洞的常见来源。 (例如,你知道ssh 使用eval 方法吗?很多人不知道,但如果你不知道,你可能没有采取预防措施在命令行上传递不受信任的字符串SSH 安全!)
  • ...btw,.sh 扩展并不普遍被认为是脚本的良好形式(与库相反,库的扩展应该反映所需的特定解释器 - 因此,.bash 用于bash,.ksh 用于 ksh,.sh 用于 POSIX sh 等)。请参阅wooledge.org/~greybot/meta/.sh,了解来自 irc.freenode.org #bash 频道关于该主题的 factoid 的历史(反映社区共识)。

标签: bash shell


【解决方案1】:

考虑:

#!/bin/bash

# use printf %q to generate a command line identical to what we're actually doing
printf "Running in each directory: " >&2
printf '%q ' "$@" >&2
echo >&2

# use && -- we don't want to execute the command if cd into a given directory failed!
for d in ./*/; do
    (cd "$d" && echo "$PWD" >&2 && "$@")
done

这更可预测:它传递 exact 参数列表,因此对于一般命令,您可以自然地引用它。 (这与使用 find -exec 或其他使用文字传递参数列表调用 execv*-family 的工具所获得的行为完全相同;因此,这意味着您获得与 sudo、@987654325 相同的行为@、chrootsetsid 等)。

对于单个命令,调用看起来像您期望的那样:

dodirs grep "free  energy   TOTEN" OUTCAR

要执行 shell 指令,例如管道,显式执行 shell:

dodirs sh -c 'grep "free  energy   TOTEN" OUTCAR | tail -n 1'
#      ^^ ^^

...或者,如果您愿意让调用者依赖于实现细节(例如,这是用 shell 实现的,以及正是 which 它是用 shell 实现的),请使用eval:

dodirs eval 'grep "free  energy   TOTEN" OUTCAR | tail -n 1'
#      ^^^^

这可能需要做更多的工作,但它使您符合标准 UNIX 约定,并且避免了在调用者未能将其参数引用为 eval-safe 时出现 shell 注入漏洞的风险。

【讨论】:

  • 我试过了,它似乎确实有效。 >&2 是什么意思?
  • >&2 重定向到标准错误,因此标准输出仅包含您正在调用的实际工具的输出。 (stdout 应该只用于 output,而 stderr 是日志等的正确位置)。
  • 啊!好的。我也不清楚为什么我需要传入 sh -c。它将整个单引号区域解释为 sh 的单个参数?为什么不简单地单引号 grep + tail 工作?
  • "$@" 从字面上获取您的参数列表,并且 [如果没有内置的 shell 或与名称匹配的] 调用 execv*-family 系统调用与它们 - 一个操作系统指令,告诉内核使用这些参数启动一个单独的可执行文件。 execv 操作不涉及 shell,除非您明确创建或调用一个,因此没有任何东西可以解释任何管道、重定向等。
  • 这实际上是一个特性,而不是一个错误:如果没有 shell 解释内容,那么您不必担心 如何 shell 会解释该内容。您无需担心文件是否具有恶意名称,例如,使用touch '/tmp/$(rm -rf $HOME)' 创建的文件,以及该名称是否会被解释:没有外壳,所以没有替换,因此风险可以避免,除非您故意创建它(在这种情况下,您希望知道自己在做什么以及如何安全地处理不受信任的内容)。
【解决方案2】:

引号会消失,因为一旦 shell 识别出要作为参数传递给脚本的单词,它们就不再需要了。在您的脚本中,$1grep$2free energy TOTEN,等等。

您确实需要转义管道(使用反斜杠 \| 或引用 '|'),以便它作为参数传递给 eval。 p>

dodirs.sh grep "free  energy   TOTEN" OUTCAR \| tail -1

【讨论】:

    猜你喜欢
    • 2012-11-13
    • 2011-11-25
    • 1970-01-01
    • 2019-10-13
    • 2020-07-25
    • 1970-01-01
    • 2015-10-31
    • 2022-08-15
    • 1970-01-01
    相关资源
    最近更新 更多