Bash: Aspas sendo removidas quando um comando é passado como argumento para uma função

6

Estou tentando implementar um tipo de mecanismo de execução a seco para meu script e encarar a questão de as cotações serem removidas quando um comando é passado como um argumento para uma função e resultando em um 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' [email protected]

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 echo:

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

Resultado:

su: invalid option -- 1

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

    
por Shoaibi 14.05.2011 / 00:00

5 respostas

4

Tente usar \" em vez de apenas " .

    
por 14.05.2011 / 00:41
3

"$@" deve funcionar. Na verdade, funciona para mim neste caso de teste simples:

dry_run()
{
    "$@"
}

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

email_admin

Saída:

./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 $5 a dry_run() . Ele irá mostrar tudo depois de -c

    
por 14.05.2011 / 01:23
3

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

No entanto, se você quiser apenas imprimir uma string que possa ser copiada e colada para repetir o comando, existem duas abordagens diferentes que você pode seguir:

  • Crie uma cadeia de comando para ser executada por meio de eval e passe essa sequência para dry_run
  • Cite os caracteres especiais do comando em dry_run antes de imprimir

Usando eval

Veja como você pode usar eval para 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"
}

Saída:

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

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

Citando Caracteres Especiais

Esta abordagem permite que você escreva código mais naturalmente, mas a saída é mais difícil para humanos lerem por causa da maneira rápida e suja que o shell_quote é implementado:

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

Saída:

'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_quote para caracteres especiais com escape de barra invertida em vez de agrupar tudo em aspas simples, mas é difícil fazer isso corretamente.

Se você fizer a abordagem shell_quote , poderá construir o comando para passar para su de maneira mais segura. Os itens a seguir funcionariam mesmo se ${GIT_WORK_TREE} , ${mail_subject} ou ${admin_email} contivessem 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"
}

Saída:

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

Isso é complicado, você pode tentar essa 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ê define DRY_RUN como em branco ou "echo" na parte superior do seu script e faz isso ou simplesmente faz eco.

    
por 14.05.2011 / 00:47
0

Bom desafio :) Deve ser "fácil" se você tiver bash recente o suficiente para suportar $LINENO e $BASH_SOURCE

Aqui está minha primeira tentativa, esperando que ela 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 '1''" #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
    
por 03.12.2012 / 13:39