使用包含引号的字符串作为 bash 中命令的参数,无需额外转义
Using a string which contains quotes as parameter to a command in bash without additional escaping
背景
我想做的是在查找命令中排除 git 存储库的所有子模块。我知道我可以像这样排除单个目录:
find . -not -path './$submodule/*'
所以我构建了一个生成大量这些语句并存储它们的命令:
EXCLUDES=$(for submodule in $(git submodule status | awk '{print }'); do
echo -n "-not -path './$submodule/*' ";
done)
问题
但是当我运行find . $EXCLUDES
时,这不起作用。我怀疑这是因为我不理解的 bash 引用策略。例如,假设 (#
标记输出):
tree .
# .
# ├── bar
# │ └── baz.scala
# └── foo.scala
set -x
EXCLUDES="-not -path './bar/*'"
find . -type f $EXCLUDES
# + find . -not -path ''\''./bar/*'\''' <---- weird stuff
# foo.scala
# baz.scala
find . -type f -not -path './bar/*'
# + find . -type f -not -path './bar/*'
# foo.scala
我如何告诉 bash 不要使用它所做的奇怪的引用(见上面标记的行)?
编辑:@eddiem 建议使用 git ls-files
,我将在这个具体案例中这样做。但我仍然对在一般情况下如何执行此操作感兴趣,在这种情况下,我有一个带引号的变量并想将其用作命令的参数。
您注意到的 "weird stuff" 是因为 bash 仅扩展 $EXCLUDES
一次,方法是代入您存储在 EXCLUDES
中的值。它不会像在命令行上指定带引号的字符串时那样递归处理 EXCLUDES
的内容以删除单引号。相反,bash 转义 $EXCLUDES
中的特殊字符,假设您希望它们在那里:
-not -path './bar/*'
变成
-not -path ''\''./bar/*'\'''
^^ ^^ escaped single quotes
^^ ^^ random empty strings I'm actually not sure about
^ ^ single quotes around the rest of your text.
所以,正如@Jean-FrançoisFabre 所说,如果你在 EXCLUDES=...
中去掉单引号,你将不会得到奇怪的东西。
那么为什么第一个 find
没有按预期工作?因为 bash 将 $EXCLUDES
扩展为单个单词,即 argv
的单个元素被传递给 find
.* 但是, find
期望它的参数是单独的词。结果,find
没有达到您的预期。
据我所知,执行此类操作的最可靠方法是使用数组:
declare -a EXCLUDES #make a new array
EXCLUDES+=("-not" "-path" './bar/*')
# single-quotes ^ ^ so we don't glob when creating the array
并且您可以重复 += 行任意次数以排除您想要的内容。然后,使用这些:
find . -type f "${EXCLUDES[@]}"
"${name[@]}"
形式, 和 所有标点符号,将数组的每个元素扩展为一个单独的单词,但不会进一步扩展这些单词。所以 ./bar/*
会保持原样,不会被全局化。 (如果你确实想要 globbing,find . -type f ${EXCLUDES[@]}
(没有 ""
)将扩展数组的每个元素。)
编辑 顺便说一下,要查看数组中的内容,请执行 set|grep EXCLUDES
。您将每个元素单独列出。您也可以执行 echo "${EXCLUDES[@]}"
,但我发现这对调试用处不大,因为它不显示索引。
* 请参阅 the man page 的 "expansion" 部分。 "Parameter expansion," 扩展以 $
开头的内容不能更改命令行上的单词数 — 除了 "$@"
和 "${name[@]}"
.
背景
我想做的是在查找命令中排除 git 存储库的所有子模块。我知道我可以像这样排除单个目录:
find . -not -path './$submodule/*'
所以我构建了一个生成大量这些语句并存储它们的命令:
EXCLUDES=$(for submodule in $(git submodule status | awk '{print }'); do
echo -n "-not -path './$submodule/*' ";
done)
问题
但是当我运行find . $EXCLUDES
时,这不起作用。我怀疑这是因为我不理解的 bash 引用策略。例如,假设 (#
标记输出):
tree .
# .
# ├── bar
# │ └── baz.scala
# └── foo.scala
set -x
EXCLUDES="-not -path './bar/*'"
find . -type f $EXCLUDES
# + find . -not -path ''\''./bar/*'\''' <---- weird stuff
# foo.scala
# baz.scala
find . -type f -not -path './bar/*'
# + find . -type f -not -path './bar/*'
# foo.scala
我如何告诉 bash 不要使用它所做的奇怪的引用(见上面标记的行)?
编辑:@eddiem 建议使用 git ls-files
,我将在这个具体案例中这样做。但我仍然对在一般情况下如何执行此操作感兴趣,在这种情况下,我有一个带引号的变量并想将其用作命令的参数。
您注意到的 "weird stuff" 是因为 bash 仅扩展 $EXCLUDES
一次,方法是代入您存储在 EXCLUDES
中的值。它不会像在命令行上指定带引号的字符串时那样递归处理 EXCLUDES
的内容以删除单引号。相反,bash 转义 $EXCLUDES
中的特殊字符,假设您希望它们在那里:
-not -path './bar/*'
变成
-not -path ''\''./bar/*'\'''
^^ ^^ escaped single quotes
^^ ^^ random empty strings I'm actually not sure about
^ ^ single quotes around the rest of your text.
所以,正如@Jean-FrançoisFabre 所说,如果你在 EXCLUDES=...
中去掉单引号,你将不会得到奇怪的东西。
那么为什么第一个 find
没有按预期工作?因为 bash 将 $EXCLUDES
扩展为单个单词,即 argv
的单个元素被传递给 find
.* 但是, find
期望它的参数是单独的词。结果,find
没有达到您的预期。
据我所知,执行此类操作的最可靠方法是使用数组:
declare -a EXCLUDES #make a new array
EXCLUDES+=("-not" "-path" './bar/*')
# single-quotes ^ ^ so we don't glob when creating the array
并且您可以重复 += 行任意次数以排除您想要的内容。然后,使用这些:
find . -type f "${EXCLUDES[@]}"
"${name[@]}"
形式, 和 所有标点符号,将数组的每个元素扩展为一个单独的单词,但不会进一步扩展这些单词。所以 ./bar/*
会保持原样,不会被全局化。 (如果你确实想要 globbing,find . -type f ${EXCLUDES[@]}
(没有 ""
)将扩展数组的每个元素。)
编辑 顺便说一下,要查看数组中的内容,请执行 set|grep EXCLUDES
。您将每个元素单独列出。您也可以执行 echo "${EXCLUDES[@]}"
,但我发现这对调试用处不大,因为它不显示索引。
* 请参阅 the man page 的 "expansion" 部分。 "Parameter expansion," 扩展以 $
开头的内容不能更改命令行上的单词数 — 除了 "$@"
和 "${name[@]}"
.