Bash: le virgolette vengono rimosse quando un comando viene passato come argomento a una funzione


8

Sto cercando di implementare un tipo di meccanismo a secco per il mio script e sto affrontando il problema delle virgolette che vengono rimosse quando un comando viene passato come argomento a una funzione e con conseguente comportamento imprevisto.

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

L'output è:

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

Previsto:

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

Con printf abilitato anziché echo:

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

Risultato:

su: invalid option -- 1

Questo non dovrebbe essere il caso se le virgolette rimanessero dove sono state inserite. Ho anche provato a usare "eval", non molta differenza. Se rimuovo la chiamata dry_run in email_admin e quindi eseguo lo script, funziona benissimo.


Risposte:



4

"$@"dovrebbe funzionare. In effetti funziona per me in questo semplice test case:

dry_run()
{
    "$@"
}

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

email_admin

Produzione:

./foo.sh 
a
b

Modificato per aggiungere: l'output di echo $@è corretto. Il "è un meta-carattere e non fa parte del parametro. Puoi provare che funziona correttamente aggiungendo echo $5a dry_run(). Produrrà tutto dopo-c


4

Questo non è un problema banale. Shell esegue la rimozione delle virgolette prima di chiamare la funzione, quindi non c'è modo che la funzione possa ricreare le virgolette esattamente come le hai digitate.

Tuttavia, se vuoi solo essere in grado di stampare una stringa che può essere copiata e incollata per ripetere il comando, puoi adottare due diversi approcci:

  • Costruire una stringa di comando da eseguire evale passare quella stringa adry_run
  • Cita i caratteri speciali del comando dry_runprima di stampare

utilizzando eval

Ecco come è possibile utilizzare evalper stampare esattamente ciò che viene eseguito:

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

Produzione:

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

Nota la folle quantità di preventivi: hai un comando all'interno di un comando all'interno di un comando, che diventa brutto rapidamente. Attenzione: il codice sopra riportato avrà problemi se le variabili contengono spazi bianchi o caratteri speciali (come virgolette).

Citando personaggi speciali

Questo approccio consente di scrivere il codice in modo più naturale, ma l'output è più difficile da leggere per gli umani a causa dell'implementazione del metodo rapido e sporco 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"
}

Produzione:

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

Puoi migliorare la leggibilità dell'output cambiando shell_quotein caratteri speciali di backslash invece di racchiudere tutto tra virgolette singole, ma è difficile farlo correttamente.

Se segui l' shell_quoteapproccio, puoi costruire il comando a cui passare suin modo più sicuro. I seguenti funzionerebbero anche se ${GIT_WORK_TREE}, ${mail_subject}o ${admin_email}contenessero caratteri speciali (virgolette singole, spazi, asterischi, punti e virgola, ecc.):

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

Produzione:

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

2

È difficile, potresti provare questo altro approccio che ho visto:

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

in questo modo hai semplicemente impostato DRY_RUN su vuoto o "echo" nella parte superiore del tuo script e lo fa o fa semplicemente eco.


0

Bella sfida :) Dovrebbe essere "facile" se hai abbastanza recente da supportare $LINENOe$BASH_SOURCE

Ecco il mio primo tentativo, sperando che soddisfi le tue esigenze:

#!/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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.