Bash: les citations sont décapées lorsqu'une command est passée comme argument à une fonction

J'essaie de mettre en place un mécanisme de démarrage à sec pour mon script et faire face à la question de quitter les citations lorsque la command est transmise en tant qu'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' user@domain.com" 

Avec printf activé au lieu d'é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 des devis restnt là où ils ont été 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, puis exécutez le script, cela fonctionne très bien.

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

"$@" devrait fonctionner. 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 

Modifié pour append: 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

Ce n'est pas un problème sortingvial. Shell effectue le retrait de devis avant d'appeler la fonction, donc il n'y a aucun moyen de recréer les citations exactement comme vous les avez tapées.

Cependant, si vous souhaitez simplement imprimer une string qui peut être copiée et collée pour répéter la command, il existe deux approches différentes que vous pouvez prendre:

  • Créez une string de command à exécuter via eval et passez cette string à dry_run
  • Citer les caractères spéciaux de la command dans dry_run avant d'imprimer

Utilisation d' 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 command à l'intérieur d'une command dans une command, ce qui devient moche 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).

Citer des caractères spéciaux

Cette approche vous permet d'écrire le code plus naturellement, mais la sortie est plus difficile pour les humains à lire en raison de la manière rapide et sale de shell_quote est 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" } 

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 en shell_quote -échappée, en évitant les caractères spéciaux au lieu d'envelopper tout en guillemets simples, mais il est difficile de le faire correctement.

Si vous faites l'approche shell_quote , vous pouvez build la command pour passer à su de manière plus sûre. Le suivant fonctionnerait même si ${GIT_WORK_TREE} , ${mail_subject} ou ${admin_email} contenait 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'\''' 

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

 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, il suffit de définir DRY_RUN en blanc ou "echo" en haut de votre script et il le fait ou l'échoue.

Bon défi 🙂 Il devrait être «facile» si vous avez assez bash pour soutenir $LINENO et $BASH_SOURCE

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