web-dev-qa-db-ja.com

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]"

Echoの代わりにprintfを有効にした場合:

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呼び出しを削除してからスクリプトを実行すると、うまく機能します。

8
Shoaibi

\"の代わりに"を使用してみてください。

5
James

これはささいな問題ではありません。 Shellは、関数を呼び出す前に引用の削除を実行するため、入力したとおりに関数が引用を正確に再作成することはできません。

ただし、コピーして貼り付けてコマンドを繰り返すことができる文字列を出力できるようにする場合は、次の2つの方法があります。

  • 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
Richard Hansen

_"$@"_が機能するはずです。実際、この単純なテストケースでは、私にとってはうまくいきます。

_dry_run()
{
    "$@"
}

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

email_admin
_

出力:

_./foo.sh 
a
b
_

追加のために編集:_echo $@_の出力は正しいです。 _"_はメタ文字であり、パラメーターの一部ではありません。 _echo $5_をdry_run()に追加することで、正しく機能していることを証明できます。 _-c_以降のすべてを出力します

4
Mark Wagner

それはトリッキーです、私が見たこの他のアプローチを試すかもしれません:

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を空白または「エコー」に設定するだけで、スクリプトが実行するか、単にエコーします。

2
Steve Kehlet

いい挑戦:) $LINENO$BASH_SOURCEをサポートするのに十分最近のbashがあれば、「簡単」になるはずです。

これがあなたのニーズに合うことを願って私の最初の試みです:

#!/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
0
Olivier Dulac