Bash: les citations sont supprimées lorsqu'une commande est passée en argument à une fonction

8

J'essaie d'implémenter un type de mécanisme de fonctionnement à sec pour mon script et je suis confronté au problème de suppression des guillemets lorsqu'une commande est passée en argument à une fonction et entraîne un comportement inattendu.

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

La sortie est:

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

Attendu:

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

Avec printf activé au lieu de l'écho:

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

Résultat:

su: invalid option -- 1

Cela ne devrait pas être le cas si les guillemets restaient là où ils étaient insérés. J'ai également essayé d'utiliser "eval", pas beaucoup de différence. Si je supprime l'appel dry_run dans email_admin et que j'exécute ensuite le script, cela fonctionne très bien.

Shoaibi
la source

Réponses:

5

Essayez d'utiliser \"au lieu de simplement ".

James
la source
4

"$@"devrait marcher. En fait, cela fonctionne pour moi dans ce cas de test simple:

dry_run()
{
    "$@"
}

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

email_admin

Production:

./foo.sh 
a
b

Modifié pour ajouter: la sortie de echo $@est correcte. Le "est un méta-caractère et ne fait pas partie du paramètre. Vous pouvez prouver qu'il fonctionne correctement en ajoutant echo $5à dry_run(). Il affichera tout après-c

Mark Wagner
la source
4

Ce n'est pas un problème trivial. Shell effectue la suppression des guillemets avant d'appeler la fonction, il n'y a donc aucun moyen que la fonction puisse recréer les guillemets exactement comme vous les avez tapés.

Cependant, si vous voulez simplement pouvoir imprimer une chaîne qui peut être copiée et collée pour répéter la commande, vous pouvez adopter deux approches différentes:

  • Créez une chaîne de commande à exécuter evalet transmettez-la àdry_run
  • Citez les caractères spéciaux de la commande dry_runavant d'imprimer

En utilisant eval

Voici comment vous pouvez utiliser evalpour imprimer exactement ce qui est exécuté:

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

Production:

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

Notez la quantité folle de citations - vous avez une commande dans une commande dans une commande, qui devient laide rapidement. Attention: le code ci-dessus aura des problèmes si vos variables contiennent des espaces ou des caractères spéciaux (comme des guillemets).

Citation de caractères spéciaux

Cette approche vous permet d'écrire du code plus naturellement, mais la sortie est plus difficile à lire pour les humains car la méthode rapide et sale shell_quoteest implémentée:

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

Production:

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

Vous pouvez améliorer la lisibilité de la sortie en shell_quoteremplaçant les caractères spéciaux par une barre oblique inverse au lieu de tout mettre entre guillemets simples, mais c'est difficile à faire correctement.

Si vous faites l' shell_quoteapproche, vous pouvez construire la commande à laquelle passer de sumanière plus sûre. Ce qui suit fonctionnerait même si ${GIT_WORK_TREE}, ${mail_subject}ou ${admin_email}contenait des caractères spéciaux (guillemets simples, des espaces, des astérisques, des points - virgules, 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"
}

Production:

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

C'est délicat, vous pourriez essayer cette autre approche que j'ai vue:

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

de cette façon, vous définissez simplement DRY_RUN sur blanc ou "écho" en haut de votre script et il le fait ou le fait simplement écho.

Steve Kehlet
la source
0

Beau défi :) Ça devrait être "facile" si vous avez assez bash pour supporter $LINENOet$BASH_SOURCE

Voici ma première tentative, en espérant qu'elle convienne à vos besoins:

#!/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
la source