Tente usar \"
em vez de apenas "
.
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.
"$@"
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
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:
eval
e passe essa sequência para dry_run
dry_run
antes de imprimir 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).
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]'\'''
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.
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