Bash: las citas se eliminan cuando se pasa un comando como argumento a una función

8

Estoy tratando de implementar un tipo de mecanismo de ejecución en seco para mi script y enfrento el problema de que las comillas se eliminen cuando se pasa un comando como argumento a una función y resulta en un comportamiento inesperado.

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

Salida es:

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

Esperado:

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

Con printf habilitado en lugar de echo:

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

Resultado:

su: invalid option -- 1

Ese no debería ser el caso si las comillas permanecieran donde se insertaron. También he intentado usar "eval", no hay mucha diferencia. Si elimino la llamada dry_run en email_admin y luego ejecuto el script, funciona muy bien.

Shoaibi
fuente

Respuestas:

5

Intenta usar en \"lugar de solo ".

James
fuente
4

"$@"Deberia trabajar. De hecho, me funciona en este caso de prueba simple:

dry_run()
{
    "$@"
}

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

email_admin

Salida:

./foo.sh 
a
b

Editado para agregar: la salida de echo $@es correcta. El "es un metacarácter y no forma parte del parámetro. Puede probar que funciona correctamente agregando echo $5a dry_run(). Producirá todo después-c

Mark Wagner
fuente
4

Este no es un problema trivial. Shell realiza la eliminación de comillas antes de llamar a la función, por lo que no hay forma de que la función pueda recrear las comillas exactamente como las escribió.

Sin embargo, si solo desea poder imprimir una cadena que se puede copiar y pegar para repetir el comando, hay dos enfoques diferentes que puede tomar:

  • Cree una cadena de comando para ejecutar evaly pase esa cadena adry_run
  • Cite los caracteres especiales del comando dry_runantes de imprimir

Utilizando eval

Aquí le mostramos cómo podría usar evalpara imprimir exactamente lo que se ejecuta:

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

Salida:

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

Tenga en cuenta la cantidad loca de citas: tiene un comando dentro de un comando dentro de un comando, que se pone feo rápidamente. Cuidado: el código anterior tendrá problemas si sus variables contienen espacios en blanco o caracteres especiales (como comillas).

Citando personajes especiales

Este enfoque le permite escribir código de forma más natural, pero la salida es más difícil de leer para los humanos debido a que shell_quotese implementa la forma rápida y sucia :

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

Salida:

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

Puede mejorar la legibilidad de la salida cambiando shell_quotea caracteres especiales de barra invertida en lugar de envolver todo entre comillas simples, pero es difícil hacerlo correctamente.

Si realiza el shell_quoteenfoque, puede construir el comando para pasar de suuna manera más segura. Lo siguiente funcionaría incluso si ${GIT_WORK_TREE}, ${mail_subject}o ${admin_email}contuviera caracteres especiales (comillas simples, espacios, asteriscos, punto y coma, 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"
}

Salida:

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

Eso es complicado, puedes probar este otro enfoque que he 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"
    }

de esa manera, simplemente configura DRY_RUN en blanco o "echo" en la parte superior de su script y lo hace o simplemente hace eco.

Steve Kehlet
fuente
0

Bonito desafío :) Debería ser "fácil" si tienes bash lo suficientemente reciente como para soportar $LINENOy$BASH_SOURCE

Aquí está mi primer intento, esperando que se adapte a sus necesidades:

#!/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
fuente