Bash:当一个命令作为parameter passing给一个函数时,引号被剥离

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

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调用,然后运行脚本,它工作得很好。

尝试使用\"而不是"

"$@"应该可以工作。 事实上,在这个简单的testing案例中,它对我很有用:

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

输出:

 ./foo.sh a b 

编辑添加: echo $@的输出是正确的。 "是一个元字符而不是参数的一部分,你可以通过在dry_run()添加echo $5来certificate它是正常工作的,它会在-c

这不是一个小问题。 在调用函数之前,Shell会执行引用移除操作,所以当你input这些引用时,函数无法重新build立引号。

但是,如果您只是希望能够打印出可以复制并粘贴的string以重复执行该命令,则可以采取两种不同的方法:

  • 构build一个通过eval运行的命令string,并将该string传递给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]" 

注意疯狂的引用量 – 你在一个命令中有一个命令,这个命令会很快变得丑陋。 注意:如果你的variables包含空白或特殊字符(如引号),上面的代码将会有问题。

引用特殊字符

这种方法使您能够更自然地编写代码,但是由于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方法,则可以构build命令以更安全的方式传递给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]'\''' 

这很棘手,你可以尝试我看到的另一种方法:

 DRY_RUN= #DRY_RUN=echo .... 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" } 

这样,您只需将DRY_RUN设置为空白或在脚本顶部“回显”,然后执行它或者只是回应它。

不错的挑战:)如果你有足够的bash来支持$LINENO$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