Bash: Anführungszeichen werden entfernt, wenn ein Befehl als Argument an eine Funktion übergeben wird

8

Ich versuche, einen Trockenlaufmechanismus für mein Skript zu implementieren, und stelle mich dem Problem, dass Anführungszeichen entfernt werden, wenn ein Befehl als Argument an eine Funktion übergeben wird und zu unerwartetem Verhalten führt.

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

Ausgabe ist:

su - webuser1 -c cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' user@domain.com

Erwartet:

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

Wenn printf anstelle von echo aktiviert ist:

su - webuser1 -c cd\ /home/webuser1/public_html\ \&\&\ git\ log\ -1\ -p\|mail\ -s\ \'Git\ deployment\ on\ webuser1\'\ user@domain.com

Ergebnis:

su: invalid option -- 1

Dies sollte nicht der Fall sein, wenn Anführungszeichen dort verbleiben, wo sie eingefügt wurden. Ich habe auch versucht, "eval" zu verwenden, nicht viel Unterschied. Wenn ich den dry_run-Aufruf in email_admin entferne und dann das Skript ausführe, funktioniert es hervorragend.

Shoaibi
quelle

Antworten:

5

Versuchen Sie es mit \"statt nur ".

James
quelle
4

"$@"sollte arbeiten. Tatsächlich funktioniert es bei mir in diesem einfachen Testfall:

dry_run()
{
    "$@"
}

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

email_admin

Ausgabe:

./foo.sh 
a
b

Zum Hinzufügen bearbeitet: Die Ausgabe von echo $@ist korrekt. Das "ist ein Metazeichen und nicht Teil des Parameters. Sie können beweisen , dass es richtig funktioniert durch Zugabe echo $5zu dry_run(). Es wird alles danach ausgegeben-c

Mark Wagner
quelle
4

Dies ist kein triviales Problem. Shell führt vor dem Aufrufen der Funktion eine Anführungszeichenentfernung durch, sodass die Funktion die Anführungszeichen auf keinen Fall genau so neu erstellen kann, wie Sie sie eingegeben haben.

Wenn Sie jedoch nur eine Zeichenfolge ausdrucken möchten, die kopiert und eingefügt werden kann, um den Befehl zu wiederholen, können Sie zwei verschiedene Ansätze wählen:

  • Erstellen Sie eine Befehlszeichenfolge, über die ausgeführt werden soll, evalund übergeben Sie diese Zeichenfolge andry_run
  • Geben Sie dry_runvor dem Drucken die Sonderzeichen des Befehls an

Verwenden von eval

So können Sie evalgenau drucken, was ausgeführt wird:

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

Ausgabe:

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

Beachten Sie die verrückte Menge an Zitaten - Sie haben einen Befehl innerhalb eines Befehls innerhalb eines Befehls, der schnell hässlich wird. Achtung: Der obige Code hat Probleme, wenn Ihre Variablen Leerzeichen oder Sonderzeichen (wie Anführungszeichen) enthalten.

Sonderzeichen zitieren

Dieser Ansatz ermöglicht es Ihnen, Code natürlicher zu schreiben, aber die Ausgabe ist für Menschen schwieriger zu lesen, da die schnelle und schmutzige Methode shell_quoteimplementiert ist:

# 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"
}

Ausgabe:

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

Sie können die Lesbarkeit der Ausgabe verbessern, indem Sie shell_quotezu Sonderzeichen mit umgekehrten Schrägstrichen wechseln , anstatt alles in einfache Anführungszeichen zu setzen. Dies ist jedoch schwierig.

Wenn Sie den shell_quoteAnsatz ausführen, können Sie den Befehl so konstruieren, dass suer sicherer übergeben wird. Die folgende funktionieren würde, auch wenn ${GIT_WORK_TREE}, ${mail_subject}oder ${admin_email}enthalten sind Sonderzeichen (Apostrophe, Leerzeichen, Sternchen, Semikolons, etc.):

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

Ausgabe:

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

Das ist schwierig, Sie könnten diesen anderen Ansatz ausprobieren, den ich gesehen habe:

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

Auf diese Weise setzen Sie DRY_RUN oben in Ihrem Skript entweder auf leer oder auf "Echo" und es tut es entweder oder gibt es einfach wieder.

Steve Kehlet
quelle
0

Schöne Herausforderung :) Es sollte "einfach" sein, wenn Sie Bash in letzter Zeit genug haben, um $LINENOund zu unterstützen$BASH_SOURCE

Hier ist mein erster Versuch, in der Hoffnung, dass er Ihren Bedürfnissen entspricht:

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