Bash: Cotações sendo retiradas quando um comando é passado como argumento para uma função

8

Estou tentando implementar um tipo de mecanismo de execução a seco para o meu script e enfrentando o problema de aspas sendo retiradas quando um comando é passado como argumento para uma função e resultando em comportamento 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"
    }

A saída é:

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

Com printf ativado em vez de eco:

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

Esse não deve ser o caso se as aspas permanecerem onde foram inseridas. Eu também tentei usar "eval", não há muita diferença. Se eu remover a chamada dry_run em email_admin e executar o script, ele funcionará muito bem.

Shoaibi
fonte

Respostas:

5

Tente usar em \"vez de apenas ".

James
fonte
4

"$@"Deveria trabalhar. De fato, funciona para mim neste caso de teste simples:

dry_run()
{
    "$@"
}

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

email_admin

Resultado:

./foo.sh 
a
b

Editado para adicionar: a saída de echo $@está correta. O "é um meta-caractere e não faz parte do parâmetro. Você pode provar que está funcionando corretamente adicionando echo $5a dry_run(). Ele produzirá tudo depois-c

Mark Wagner
fonte
4

Este não é um problema trivial. O Shell executa a remoção de cotação antes de chamar a função, portanto, não há como a função recriar as cotações exatamente como você as digitou.

No entanto, se você quiser imprimir uma sequência que pode ser copiada e colada para repetir o comando, há duas abordagens diferentes que você pode seguir:

  • Crie uma sequência de comandos a ser executada evale passe essa sequência paradry_run
  • Cite os caracteres especiais do comando dry_runantes de imprimir

Usando eval

Veja como você pode usar evalpara imprimir exatamente o que é executado:

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

Resultado:

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

Observe a quantidade louca de citações - você tem um comando dentro de um comando, que fica feio rapidamente. Cuidado: o código acima terá problemas se suas variáveis ​​contiverem espaços em branco ou caracteres especiais (como aspas).

Citando caracteres especiais

Essa abordagem permite que você escreva código de forma mais natural, mas a saída é mais difícil para os humanos lerem devido à maneira rápida e suja de shell_quoteser implementada:

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

Resultado:

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

Você pode melhorar a legibilidade da saída alterando shell_quotepara caracteres especiais de escape com barra invertida em vez de agrupar tudo entre aspas simples, mas é difícil fazer isso corretamente.

Se você fizer a shell_quoteabordagem, poderá construir o comando para transmitir de suuma maneira mais segura. O seguinte funcionaria mesmo se ${GIT_WORK_TREE}, ${mail_subject}ou ${admin_email}contivesse caracteres especiais (aspas simples, espaços, asteriscos, ponto e vírgula, 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"
}

Resultado:

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

Isso é complicado, você pode tentar esta outra abordagem que já vi:

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

Dessa forma, você apenas define DRY_RUN como em branco ou "echo" na parte superior do seu script e ele faz ou apenas faz eco.

Steve Kehlet
fonte
0

Bom desafio :) Deve ser "fácil" se você bash recente o suficiente para apoiar $LINENOe$BASH_SOURCE

Aqui está minha primeira tentativa, esperando que atenda às suas necessidades:

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