Я пытаюсь реализовать механизм пробного прогона для своего скрипта и сталкиваюсь с проблемой удаления кавычек, когда команда передается в качестве аргумента функции, что приводит к неожиданному поведению.
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"
}
Выход:
su - webuser1 -c cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' user@domain.com
Ожидается:
su - webuser1 -c "cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' user@domain.com"
С включенным printf вместо echo:
su - webuser1 -c cd\ /home/webuser1/public_html\ \&\&\ git\ log\ -1\ -p\|mail\ -s\ \'Git\ deployment\ on\ webuser1\'\ user@domain.com
Результат:
su: invalid option -- 1
Этого не должно быть, если кавычки остались там, где они были вставлены. Я также пробовал использовать eval, особой разницы нет. Если я удалю вызов dry_run в email_admin, а затем запустил скрипт, он отлично заработает.
Попробуйте использовать \"
вместо просто "
.
Это нетривиальная проблема. Shell выполняет удаление кавычек перед вызовом функции, поэтому функция не может воссоздать кавычки точно в том виде, в котором вы их ввели.
Однако, если вы просто хотите распечатать строку, которую можно скопировать и вставить, чтобы повторить команду, вы можете использовать два разных подхода:
eval
и передайте эту строку в dry_run
dry_run
перед печатьюeval
Вот как вы могли бы использовать eval
напечатать именно то, что выполняется:
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"
}
Вывод:
su - webuser1 -c "cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' user@domain.com"
Обратите внимание на безумное количество цитирований - у вас есть команда внутри команды внутри команды, которая быстро становится уродливой. Остерегайтесь: в приведенном выше коде будут проблемы, если ваши переменные содержат пробелы или специальные символы (например, кавычки).
Такой подход позволяет писать код более естественно, но вывод сложнее для чтения людьми из-за быстрого и грязного способа. 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"
}
Вывод:
'su' '-' 'webuser1' '-c' 'cd /home/webuser1/public_html && git log -1 -p|mail -s '\''Git deployment on webuser1'\'' user@domain.com'
Вы можете улучшить читаемость вывода, изменив shell_quote
использовать обратную косую черту для специальных символов вместо того, чтобы заключать все в одинарные кавычки, но это сложно сделать правильно.
Если вы сделаете shell_quote
подход, вы можете создать команду для передачи su
более безопасным способом. Следующее будет работать, даже если ${GIT_WORK_TREE}
, ${mail_subject}
, или ${admin_email}
содержат специальные символы (одинарные кавычки, пробелы, звездочки, точки с запятой и т. д.):
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"
}
Вывод:
'su' '-' 'webuser1' '-c' ''\''cd'\'' '\''/home/webuser1/public_html'\'' && git log -1 -p | '\''mail'\'' '\''-s'\'' '\''Git deployment on webuser1'\'' '\''user@domain.com'\'''
"$@"
должно сработать. На самом деле у меня это работает в этом простом тестовом примере:
dry_run()
{
"$@"
}
email_admin()
{
dry_run su - foo -c "cd /var/tmp && ls -1"
}
email_admin
Вывод:
./foo.sh
a
b
Отредактировано для добавления: вывод echo $@
правильно. В "
является метасимволом, а не частью параметра. Вы можете доказать, что он работает правильно, добавив echo $5
к dry_run()
. Он будет выводить все после -c
Это сложно, вы можете попробовать другой подход, который я видел:
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"
}
таким образом вы просто устанавливаете DRY_RUN либо на пустой, либо на «эхо» в верхней части вашего скрипта, и он либо выполняет это, либо просто повторяет его.
Хорошая задача :) Это должно быть "легко", если у вас достаточно недавнего bash для поддержки $LINENO
и $BASH_SOURCE
Вот моя первая попытка, надеюсь, она вам подходит:
#!/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