Bash:将带引号参数的命令传递给函数

Bash:将带引号参数的命令传递给函数

我有以下 bash 函数:

function exe {
    echo -e "Execute: $1"
    # Loops every 3s, outputting '...' until command finished executing
    LOOP=0
    while true;
    do
        if ! [ $LOOP == 0 ]; then echo -e "..."; fi;
        sleep 3;
        LOOP=$LOOP+1
    done & ERROR="$($2 2>&1)" # Execute the command and capture output to variable

    status=$?
    kill $!; trap 'kill $!' SIGTERM

    if [ $status -ne 0 ];
    then
        echo -e "✖ Error" >&2
        echo -e "$ERROR" >&2
    else
        echo -e "✔ Success"
    fi
    return $status
}

其目的是这样称呼它:

exe "Update apt indexes" \
    "sudo apt-get update"

哪个输出:

Execute: Update apt indexes
...
...
...
...
✔ Success

除非在传递的命令中使用带引号的字符串作为参数,否则它可以正常工作。

例如,以下内容不起作用:

exe "Create self signed certificate" \
    "sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj \"/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local\""

set -x 显示上面的命令被转换为以下命令来执行:

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj '"/C=GB/ST=London/L=London/O=Confetti' Celebrations Ltd/OU=IT 'Department/CN=dev.sign-in.confetti.local"'

这似乎拾起了许多单引号并使命令无效。

我想要一个没有这个限制的版本。有任何想法吗?

===============

在建议和其他一些错误修复之后,我的最终代码是:

exe () {
    echo -e "Execute: $1"
    LOOP=0
    while true;
    do
        if ! [ $LOOP == 0 ]; then echo -e "..."; fi;
        sleep 3;
        LOOP=$((LOOP+1))
    done & ERROR=$("${@:2}" 2>&1)
    status=$?
    kill $!; trap 'kill $!' SIGTERM

    if [ $status -ne 0 ];
    then
        echo -e "✖ Error" >&2
        echo -e "$ERROR" >&2
    else
        echo -e "✔ Success"
    fi
    return $status
}

该函数旨在作为 vagrant 配置 shell 脚本的“美化器”,并且可以调用为

exe "Update apt indexes" sudo apt-get update

输出显示为

Execute: Update apt indexes
...
...
...
...
✔ Success

持续时间少于 3 秒的命令看不到进度点输出

除非出现错误,否则您将收到错误状态和命令的完整输出。

主要目的是消除 vagrant 配置脚本在向 stderr 输出消息时显示的红线。许多命令正确地将信息输出到 stderr,因为它的目的是不应该通过管道传输到其他命令的消息。 Vagrant 将消息打印到标准输出为 这会留下许多看似错误但实际上并非错误的配置消息。

除非执行的命令返回非零状态,否则此函数不会打印到 stderr。这意味着除非命令指示失败,否则您将不会看到红色消息。当命令指示失败并显示非零消息时,我们将命令的完整输出输出到 stderr,并给出红线。

使用 shell 脚本使 vagrant 配置更加整洁,并且意味着我们确实可以留意红色消息,知道它们意味着什么。

与 vagrant 一起使用的完整函数,包括我在上面的代码片段中遗漏的一些视觉效果,可以在这里看到:https://gist.github.com/michaelward82/c1903f2b37a76975740e

使用 exe 函数的示例输出,没有错误: 使用 exe 函数的示例输出,有错误

使用 exe 函数的示例输出,有错误: 使用 exe 函数的示例输出,有错误

直接执行命令的默认输出,没有实际错误: 直接执行命令的默认输出,没有实际错误

答案1

您可能不想将整个命令作为字符串传递。我们在 shell 中有列表,作为参数列表,并且将列表作为列表传递要简单得多。

不用写exe blah "blahh cmd",而是直接写命令,就像exe blah blahh cmd.然后,当你需要直接使用整个命令时,使用切片扩展获取第一个参数之后的所有内容:ERROR=$("${@:1}" 2>&1).

传统上,人们可能会使用shift“向左”移动整个参数列表(请参阅help shift):

f(){
    local j="$1"
    shift
    echo "$j,$3"
    shift 50
    echo "$1" # guess what "$@" is now?
}

f {1..100}

但这对于 bash 显然不是必需的。

说到切片,您可能还想看看数组在bash中。


呃,仍然..您可以用来eval直接运行字符串,但这通常被认为是一件坏事,因为您允许的不仅仅是简单的命令。


作为风格提示,与 和 相比,更喜欢更短且更(POSIX-)xxx()便携function xxxfunction xxx()。在 bash 中它们是相同的。

答案2

您问题的核心问题是“如何拆分字符串” $var

“邪恶”(因为它容易出错和代码执行)的方式是使用 eval:

 eval set -- $var           ### Dangerous, not recommended, do not use.

这会在位置参数中设置分割字符串(数组稍微复杂一些)。但是变量$var不加引号(除非您真的知道自己在做什么,否则要不惜一切代价避免)使其受到“分词”(我们想要的)的影响,但这也允许“路径名扩展”发生。您可以尝试此命令(使用包含少量文件的目录)

$ var='hello * world'
$ eval set -- $var
$ echo "$@"

执行是安全的,没有外部设置值,并且扩展*只会设置位置参数中的值。

为了避免“路径名扩展”,set -f使用了 a,在这种情况下,很容易将 is 集成到命令中:

$ var='hello * world'
$ set -f
$ eval set -- $var
$ echo "$@"
hello * world

其默认 IFS 为spaceTabNew Line

如果 IFS 可以在外部设置,事情可能会变得复杂。

使用以下方法可以解决几个问题read

$ IFS=' ' read -ra arr <<<"$var"
$ echo "${arr[@]}"
hello * world

这为命令设置了 IFS(避免从外部设置 IFS),读取时不处理反斜杠(-r 选项),将所有内容放入数组变量中(-a 选项),并使用引用的变量"$var"。唯一需要注意的是单词之间重复的空格将被删除(因为 IFS 是一个空格)。这对于可执行命令行来说不是问题。

但尝试执行需要带空格的参数的命令将会失败:

$ var='date -d "-1 day" +"%Y.%m.%d-%H:%M:%S"'
$ IFS=' ' read -ra arr <<<"$var"
$ "${arr[@]}"
date: extra operand `+"%Y.%m.%d-%H:%M:%S"'

唯一真正的解决方案是从一开始就正确构建命令数组:

$ arr=( date -d "-1 day" +"%Y.%m.%d-%H:%M:%S" )
$ "${arr[@]}"
2016.03.05-00:25:17

将此解决方案视为 CSV“逗号(空格)分隔值”。

该脚本将起作用:

#!/bin/bash

function exe {
    echo "Execute: $1"
    # Loops every 3s, outputting '...' until command finished executing
    LOOP=0
    while true; do
        if [ $LOOP -gt 0 ]; then echo -e "..."; fi;
            sleep 3;
            (( LOOP++ ))
    done &

    ERROR="$("${@:2}" 2>&1)" # Execute command and capture output.
    status=$?

    kill $!; trap 'kill $!' SIGTERM

    if [ $status -ne 0 ];
    then
        echo "✖ Error" >&2
        echo "$ERROR" >&2
    else
        echo "✔ Success"
    fi
    return $status
}

cmd=( date -d '-1 day' +'%Y.%m.%d-%H:%M:%S' )
exe "give me yesterday date" "${cmd[@]}" 

cmd=( sudo apt-get update )
exe "update package list" "${cmd[@]}" 

答案3

如果参数字符串内有引号作为代码执行,则可以将参数字符串重新解析为数组,例如位置参数数组$@。这可以通过使用来实现——至少对于给定的示例... & ERROR="$( printf "%s" "$2" | xargs sh -c 'exec "$0" "$@" 2>&1' ) ...。 (在已经引用的字符串中存在额外双引号的情况,可能会导致xargs: unterminated quote消息)。

有关一些进一步的建议,请参阅:Linux/Bash:如何取消引号?

# test cases
# help :
#set -- '' "ls -ld / 'a bc'" 
set -- '' ": sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj \"/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local\""

printf "%s" "$2" | 
    xargs sh -c '
       echo "arg 0: ${0}"
       for ((i=1; i<=$#; i++)); do
          echo "arg $i: ${@:i:1}"
       done
       set -xv
       "$0" "$@"
    ' 

# output
arg 0: :
arg 1: sudo
arg 2: openssl
arg 3: req
arg 4: -x509
arg 5: -nodes
arg 6: -days
arg 7: 365
arg 8: -newkey
arg 9: rsa:2048
arg 10: -keyout
arg 11: /etc/apache2/ssl/apache.key
arg 12: -out
arg 13: /etc/apache2/ssl/apache.crt
arg 14: -subj
arg 15: /C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local
   "$0" "$@"
+ : sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj '/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local'

(顺便说一句LOOP=$LOOP+1,在你上面的代码中应该是LOOP=$((LOOP+1))。)

相关内容