Bash:当命令作为参数传递给函数时,引号会被删除

Bash:当命令作为参数传递给函数时,引号会被删除

我正在尝试为我的脚本实现一种试运行机制,并且面临当将命令作为参数传递给函数时引号被剥离并导致意外行为的问题。

dry_run () {
    echo "$@"
    #printf '%q ' "$@"

    if [ "$DRY_RUN" ]; then
        return 0
    fi

    "$@"
}


email_admin() {
    echo " Emailing admin"
    dry_run su - $target_username  -c "cd $GIT_WORK_TREE && git log -1 -p|mail -s '$mail_subject' $admin_email"
    echo " Emailed"
    }

输出为:

su - webuser1 -c cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' [email protected]

预期的:

su - webuser1 -c "cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' [email protected]"

启用 printf 而不是 echo:

su - webuser1 -c cd\ /home/webuser1/public_html\ \&\&\ git\ log\ -1\ -p\|mail\ -s\ \'Git\ deployment\ on\ webuser1\'\ [email protected]

结果:

su: invalid option -- 1

如果引号保留在插入的位置,情况就不会如此。我也尝试过使用“eval”,没什么区别。如果我删除 email_admin 中的 dry_run 调用,然后运行脚本,效果会很好。

答案1

尝试使用\"而不是仅仅"

答案2

"$@"应该可以工作。事实上,在这个简单的测试案例中,它对我来说是有效的:

dry_run()
{
    "$@"
}

email_admin()
{
    dry_run su - foo -c "cd /var/tmp && ls -1"
}

email_admin

输出:

./foo.sh 
a
b

编辑后添加: 的输出echo $@是正确的。"是元字符,而不是参数的一部分。 您可以通过将 添加到 来证明它正常工作echo $5dry_run()它将输出 之后的所有内容-c

答案3

这不是一个小问题。 Shell 在调用函数之前会删除引号,因此函数无法完全按照您输入的方式重新创建引号。

但是,如果您只是希望能够打印出可以复制和粘贴以重复该命令的字符串,那么您可以采用两种不同的方法:

  • 构建要通过其运行的命令字符串eval并将该字符串传递给dry_run
  • dry_run打印前引用命令的特殊字符

使用eval

你可以使用以下方法来eval准确打印运行的内容:

dry_run() {
    printf '%s\n' "$1"
    [ -z "${DRY_RUN}" ] || return 0
    eval "$1"
}

email_admin() {
    echo " Emailing admin"
    dry_run 'su - '"$target_username"'  -c "cd '"$GIT_WORK_TREE"' && git log -1 -p|mail -s '"'$mail_subject'"' '"$admin_email"'"'
    echo " Emailed"
}

输出:

su - webuser1  -c "cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' [email protected]"

请注意大量的引用——命令中又包含命令,这很快就会变得很丑陋。注意:如果变量包含空格或特殊字符(如引号),上述代码将会出现问题。

引用特殊字符

shell_quote这种方法使你能够更自然地编写代码,但是由于实现了快速而粗糙的方式,输出对于人类来说更难阅读:

# This function prints each argument wrapped in single quotes
# (separated by spaces).  Any single quotes embedded in the
# arguments are escaped.
#
shell_quote() {
    # run in a subshell to protect the caller's environment
    (
        sep=''
        for arg in "$@"; do
            sqesc=$(printf '%s\n' "${arg}" | sed -e "s/'/'\\\\''/g")
            printf '%s' "${sep}'${sqesc}'"
            sep=' '
        done
    )
}

dry_run() {
    printf '%s\n' "$(shell_quote "$@")"
    [ -z "${DRY_RUN}" ] || return 0
    "$@"
}

email_admin() {
    echo " Emailing admin"
    dry_run su - "${target_username}"  -c "cd $GIT_WORK_TREE && git log -1 -p|mail -s '$mail_subject' $admin_email"
    echo " Emailed"
}

输出:

'su' '-' 'webuser1' '-c' 'cd /home/webuser1/public_html && git log -1 -p|mail -s '\''Git deployment on webuser1'\'' [email protected]'

shell_quote您可以通过更改为反斜杠转义特殊字符而不是将所有内容括在单引号中来提高输出的可读性,但这很难正确完成。

如果您采用这种方法,则可以更安全地shell_quote构造要传递给的命令。即使、或包含特殊字符(单引号、空格、星号、分号等),以下内容仍可行:su${GIT_WORK_TREE}${mail_subject}${admin_email}

email_admin() {
    echo " Emailing admin"
    cmd=$(
        shell_quote cd "${GIT_WORK_TREE}"
        printf '%s' ' && git log -1 -p | '
        shell_quote mail -s "${mail_subject}" "${admin_email}"
    )
    dry_run su - "${target_username}"  -c "${cmd}"
    echo " Emailed"
}

输出:

'su' '-' 'webuser1' '-c' ''\''cd'\'' '\''/home/webuser1/public_html'\'' && git log -1 -p | '\''mail'\'' '\''-s'\'' '\''Git deployment on webuser1'\'' '\''[email protected]'\'''

答案4

$LINENO不错的挑战 :) 如果你的 bash 足够新,支持的话,这应该“很容易”,$BASH_SOURCE

这是我的第一次尝试,希望它能满足您的需要:

#!/bin/bash
#adjust the previous line if needed: on prompt, do "type -all bash" to see where it is.    
#we check for the necessary ingredients:
[ "$BASH_SOURCE" = "" ] && { echo "you are running a too ancient bash, or not running bash at all. Can't go further" ; exit 1 ; }
[ "$LINENO" = "" ] && { echo "your bash doesn't support LINENO ..." ; exit 2 ; }
# we passed the tests. 
export _tab_="`printf '\011'`" #portable way to define it. It is used below to ensure we got the correct line, whatever separator (apart from a \CR) are between the arguments

function printandexec {
   [ "$FUNCNAME" = "" ] && { echo "your bash doesn't support FUNCNAME ..." ; exit 3 ; }
   #when we call this, we should do it like so :  printandexec $LINENO / complicated_cmd 'with some' 'complex arguments | and maybe quoted subshells'
   # so : $1 is the line in the $BASH_SOURCE that was calling this function
   #    : $2 is "/" , which we will use for easy cut
   #    : $3-... are the remaining arguments (up to next ; or && or || or | or #. However, we don't care, we use another mechanism...)
   export tmpfile="/tmp/printandexec.$$" #create a "unique" tmp file
   export original_line="$1"
   #1) display & save for execution:
   sed -e "${original_line}q;d" < ${BASH_SOURCE} | grep -- "${FUNCNAME}[ ${_tab_}]*\$LINENO" | cut -d/ -f2- | tee "${tmpfile}"
   #then execute it in the *current* shell so variables, etc are all set correctly:
   source ${tmpfile}
   rm -f "${tmpfile}"; #always have last command in a function finish by ";"

}

echo "we do stuff here:"
printandexec  $LINENO  / ls -al && echo "something else" #and you can even put commentaries!
#printandexec  $LINENO / su - $target_username  -c "cd $GIT_WORK_TREE && git log -1 -p|mail -s '$mail_subject' $admin_email"
#uncommented the previous on your machine once you're confident the script works

相关内容