Question Bash: Les citations sont supprimées lorsqu'une commande est transmise en tant qu'argument à une fonction


J'essaie d'implémenter un type de mécanisme d'exécution à sec pour mon script et de résoudre le problème des guillemets supprimés lorsqu'une commande est transmise en tant qu'argument à une fonction, ce qui 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' user@domain.com"

Avec printf activé au lieu de echo:

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 sont restés là où ils ont été insérés. J'ai aussi essayé d'utiliser "eval", pas beaucoup de différence. Si je supprime l'appel dry_run dans email_admin puis lance un script, cela fonctionne très bien.


6
2018-05-13 22:00


origine


En relation: Comment utiliser une variable Bash (chaîne) contenant des guillemets dans une commande? - sampablokuper


Réponses:


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


4
2018-05-13 22:41





"$@" 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

Sortie:

./foo.sh 
a
b

Édité pour ajouter: la sortie de echo $@ est correct. le " est un méta-caractère et ne fait pas partie du paramètre. Vous pouvez prouver que cela fonctionne correctement en ajoutant echo $5 à dry_run(). Il sortira tout après -c


3
2018-05-13 23:23





Ce n'est pas un problème trivial. Shell supprime les guillemets avant d'appeler la fonction. Par conséquent, la fonction ne peut pas recréer les guillemets exactement comme vous les avez saisis.

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

  • Construire une chaîne de commande à exécuter via eval et passez cette chaîne à dry_run
  • Citez les caractères spéciaux de la commande dans dry_run avant d'imprimer

En utilisant eval

Voici comment vous pouvez utiliser eval pour 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"
}

Sortie:

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

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

Citer des caractères spéciaux

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

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

Sortie:

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

Vous pouvez améliorer la lisibilité de la sortie en modifiant shell_quote Backslash-Escape des caractères spéciaux au lieu de tout mettre entre guillemets, mais il est difficile de faire correctement.

Si vous faites le shell_quote approche, vous pouvez construire la commande à passer à su d'une manière plus sûre. Ce qui suit fonctionnerait même si ${GIT_WORK_TREE}, ${mail_subject}, ou ${admin_email} contient des caractères spéciaux (guillemets simples, espaces, astérisques, 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"
}

Sortie:

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

3
2018-06-17 19:15





C'est délicat, vous pouvez 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 "echo" en haut de votre script et le fait ou le répète.


2
2018-05-13 22:47





Beau défi :) Cela devrait être "facile" si vous avez assez bash pour supporter $LINENO et $BASH_SOURCE

Voici ma première tentative, en espérant que cela corresponde à 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

0
2017-12-03 12:39