在Bash变量内部引用未被尊重

3
我将命令的参数存储在一个变量中。我想要的最终命令是:
mock -r myconfig --define "debug_package %{nil}" --resultdir results --rebuild mypackage.src.rpm

这是我的尝试:

set -x    # for debugging

RESULTDIR=results
MOCK_CONFIG="myconfig"
MOCK_ARGS="-r $MOCK_CONFIG --define \"debug_package %{nil}\" --resultdir $RESULTDIR"
cmd="mock $MOCK_ARGS --rebuild mypackage.src.rpm"
$cmd

结果如下:

+ RESULTDIR=results
+ MOCK_CONFIG=myconfig
+ MOCK_ARGS='-r myconfig --define "debug_package %{nil}" --resultdir results'
+ cmd='mock -r myconfig --define "debug_package %{nil}" --resultdir results --rebuild mypackage.src.rpm'
+ mock -r myconfig --define '"debug_package' '%{nil}"' --resultdir results --rebuild mypackage.src.rpm
ERROR: Bad option for '--define' ("debug_package).  Use --define 'macro expr'

正如您所看到的,对于--define参数的参数没有被正确地引用。 --define认为我只传递了debug_package,这是不完整的。
我已经尝试了在定义MOCK_ARGS时使用各种引号变化,甚至尝试转义debug_package%{nil}之间的空格。
哪种引号和/或转义的组合允许我构建此参数列表并从此脚本执行命令?
编辑:
我将生成的命令存储在变量中的原因是因为它最终会传递到一个执行一些日志记录,然后执行命令的函数中。
另外,我遇到了这个常见问题解答,该解答建议我使用数组而不是变量。 我已经开始尝试使用数组,但目前还没有可行的解决方案。

这看起来像是 https://dev59.com/kUfSa4cB1Zd3GeqPAuDW 的重复。 - Nathan Fellman
https://dev59.com/kUfSa4cB1Zd3GeqPAuDW 的答案对我不起作用。 - Mike Mazur
可能是重复的问题:如何执行保存为带引号和星号字符串的bash命令 - Ciro Santilli OurBigBook.com
2个回答

4

Shell脚本可能很糟糕。在许多情况下,cmd="blah $blah";$cmdblah $blah不同。您可以尝试使用eval $cmd而不是$cmd。

尝试调用此perl脚本而不是mock:

#!/usr/bin/perl
print join("\n",@ARGV),"\n";

你可能会惊讶地发现,实际上参数列表是什么:

-r
myconfig
--define
"debug_package
%{nil}"
--resultdir
results
--rebuild
mypackage.src.rpm

虽然我猜测"set -x"的输出是试图用单引号来展示这个问题。

我最近学到的一个不错的技巧是

function f {
  echo "$@"
}

实际上,正确地将位置参数传递给f($*不能)。

看起来,“eval”可以给出您可能想要的结果:

set -x    # for debugging
RESULTDIR=results
MOCK_CONFIG="myconfig"
MOCK_ARGS="-r $MOCK_CONFIG "'--define "debug_package %{nil}" --resultdir '"$RESULTDIR"
cmd="mock $MOCK_ARGS --rebuild mypackage.src.rpm"
echo $cmd
eval $cmd

输出:

+ echo mock -r myconfig --define '"debug_package' '%{nil}"' --resultdir results --rebuild mypackage.src.rpm
mock -r myconfig --define "debug_package %{nil}" --resultdir results --rebuild mypackage.src.rpm
+ eval mock -r myconfig --define '"debug_package' '%{nil}"' --resultdir results --rebuild mypackage.src.rpm
++ mock -r myconfig --define 'debug_package %{nil}' --resultdir results --rebuild mypackage.src.rpm
-r
myconfig
--define
debug_package %{nil}
--resultdir
results
--rebuild
mypackage.src.rpm

2
谢谢 - eval 对我来说是一个快速解决方案。 - John Lehmann

4

数组是处理这类问题的最佳选择。以下是我想出来的:

log_and_run() {
    echo "$(date): running command: $*"
    "$@"
    echo "$1 completed with status $?"
}

RESULTDIR=results
MOCK_CONFIG="myconfig"
MOCK_ARGS=(-r "$MOCK_CONFIG" --define "debug_package %{nil}" --resultdir "$RESULTDIR")
cmd=(mock "${MOCK_ARGS[@]}" --rebuild mypackage.src.rpm)
log_and_run "${cmd[@]}"
# could also use: log_and_run mock "${MOCK_ARGS[@]}" --rebuild mypackage.src.rpm

请注意,$* 扩展为由空格分隔的所有参数(适用于传递给日志命令);而 "$@" 展开为所有参数作为单独的单词(如在 log_and_run 中使用,第一个参数将被视为要执行的命令,其余参数作为它的参数);同样,"${MOCK_ARGS[@]}" 展开为 MOCK_ARGS 的所有元素作为单独的单词,无论它们是否包含空格(因此,MOCK_ARGS 的每个元素都成为 cmd 的一个元素,没有引号解析混淆)。
另外,我已经引用了 MOCK_CONFIG 和 RESULTDIR 的扩展,虽然这里不必要,因为它们不包含空格,但是以防它们确实包含空格(例如,它从脚本外部传递进来?那么你应该假设它可能包含空格),这是一个好习惯。
顺便说一下,如果需要向函数传递其他参数(我将使用日志记录文件名的示例),可以使用 数组切片 将仅后面的参数拆分为用于记录/执行的命令。
log_and_run() {
    echo "$(date): running command ${*:2}" >>"$1" 
    "${@:2}"
    echo "$2 completed with status $?" >>"$1"
}
#...
log_and_run test.log "${cmd[@]}"

补充说明:如果您想在日志中引用包含空格的参数,可以“手动”构造日志字符串并进行任何引用。例如:
log_and_run() {
    local log_cmd=""
    for arg in "$@"; do
        if [[ "$arg" == *" "* || -z "$arg" ]]; then
            log_cmd="$log_cmd \"$arg\""
        else
            log_cmd="$log_cmd $arg"
        fi
    done
    echo "$(date): running command:$log_cmd"
    ...

请注意,此方法可以处理参数中的空格和空参数,但无法处理参数内部的双引号。正确处理此问题会变得非常复杂...
另外,我构建log_cmd字符串的方式会导致前面有一个空格;在echo命令中,我通过在其前面省略空格来处理此问题。如果您需要实际剪切空格,请使用"${log_cmd# }"

谢谢,这解决了我的问题!关键是将完整的“mock”命令制作成一个数组。这导致我重构了我的“log_and_run()”函数,该函数天真地使用$1来记录和运行命令,而不是$*和/或$@。我无法在日志中显示“debug_package%{nil}”周围的引号,但这是我可以接受的小细节。 - Mike Mazur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接