为什么在将字符串变量作为命令运行时不能引用它?

为什么在将字符串变量作为命令运行时不能引用它?

我的基于 Ubuntu/Debian 的 Linux 更新 POSIX shell 脚本似乎要求我不是双引号包含正在执行的存储命令的字符串变量。由于我不明白这个问题,所以想请问这是为什么?如果代码是正确的呢?

警告:SC2086,维基百科,“双引号可防止通配符和分词。”

脚本如下,突出显示有问题的部分:

#!/bin/sh

# exit script when it tries to use undeclared variables
set -u

# color definitions
readonly red=$(tput bold)$(tput setaf 1)
readonly green=$(tput bold)$(tput setaf 2)
readonly yellow=$(tput bold)$(tput setaf 3)
readonly white=$(tput bold)$(tput setaf 7)
readonly color_reset=$(tput sgr0)
# to create blocks of texts, I separate them with this line
readonly block_separator='----------------------------------------'

step_number=0
execute_jobs ()
{
    while [ ${#} -gt 1 ]
    do
        job_description=${1}
        job_command=${2}

        step_number=$(( step_number + 1 ))

        printf '%s\n' "Step #${step_number}: ${green}${job_description}${color_reset}"

        printf '%s\n' "Command: ${yellow}${job_command}${color_reset}"

        printf '%s\n' "${white}${block_separator}${color_reset}"

        # RUN THE ACTUAL COMMAND
        # ShellCheck warns me I should double quote the parameter
        # If I did, it would become a string (I think) and I'd get 'command not found' (proven)
        # As I don't understand the issue, I left it as I wrote it, without quotes
        ### shellcheck disable=SC2086
        if sudo ${job_command} # <-- HERE
        then
            printf '\n'
        else
            printf '%s\n\n' "${red}An error occurred.${color_reset}"
            exit 1
        fi

        shift 2
    done
}

execute_jobs \
    'configure packages'                         'dpkg --configure --pending' \
    'fix broken dependencies'                    'apt-get --assume-yes --fix-broken install' \
    'update cache'                               'apt-get update' \
    'upgrade packages'                           'apt-get --assume-yes upgrade' \
    'upgrade packages with possible removals'    'apt-get --assume-yes dist-upgrade' \
    'remove unused packages'                     'apt-get --assume-yes --purge autoremove' \
    'clean up old packages'                      'apt-get autoclean'

答案1

您不能在此处引用该变量:

if sudo ${job_command}

因为你想要分词。如果您引用,该命令将变为(对于您的第一步)

if sudo "dpkg --configure --pending"

sudo查找 command ,而不是带有参数和dpkg --configure --pending的命令,如其错误消息所示:dpkg--configure--pending

sudo: dpkg --configure --pending: command not found

(尝试使用额外的空格以使其更明确)。

省略引号后,shell 会分割参数,一切都会按预期工作。

答案2

按照您现在编写代码的方式,您依赖 shell 将保存命令的字符串正确拆分为带有附加选项和其他参数的命令名称。仅当您将变量扩展不加引号时,shell 才会执行此操作。

然而,shell 不会这样做正确地如果您的命令字符串包含一些应该在空格上分割的参数,而其他参数则不应该分割,或者如果它包含看起来像不应该扩展的文件名通配模式的子字符串。

脚本的另一种实现允许您正确引用所有扩展:

#!/bin/sh

step=0
execute_job () {
    step=$(( step + 1 ))
    printf 'Step #%s: "%s"\n' "$step" "$1"
    shift
    printf 'Command: %s\n' "$*"

    if sudo -- "$@"; then
        echo Done
    else
        echo 'Some error occured'
        exit 1
    fi
}

execute_job 'configure packages'      dpkg --configure --pending
execute_job 'fix broken dependencies' apt-get --assume-yes --fix-broken install
# etc.

在这里,我只execute_job关心单个工作而不是所有工作。我将函数的第一个参数视为职位描述字符串。然后我将任何进一步的参数作为要执行的命令。应执行的命令传递为一些参数,而不是单个字符串。

打印命令字符串时,我使用,它是一个单引号字符串,由位置参数和第一个字符作为分隔符(默认为空格)"$*"连接而成。$IFS

执行命令时,我使用"$@",它是单独引用的位置参数列表,以避免分词和文件名通配。

这将允许您使用该函数来做类似的事情

execute_job 'printf test' printf '%s\n' 'line 1' 'line 2'

...将line 1and打印line 2在两个单独的行上(或具有带有空格、制表符、换行符或通配符的参数的任何其他命令)。

相关内容