Bash: кавычки удаляются, когда команда передается в качестве аргумента функции

Я пытаюсь реализовать механизм пробного запуска для моего сценария и сталкиваюсь с проблемой удаления кавычек, когда команда передается в качестве аргумента функции и приводит к неожиданному поведению.

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, а затем запускаю скрипт, он отлично работает.

5 ответов

Решение

Попробуйте использовать \" вместо просто ",

"$@" должно сработать. На самом деле это работает для меня в этом простом тестовом примере:

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

Это не тривиальная проблема. Оболочка выполняет удаление кавычек перед вызовом функции, поэтому функция никак не может воссоздать кавычки в точности так, как вы их ввели.

Однако, если вы просто хотите распечатать строку, которую можно скопировать и вставить, чтобы повторить команду, вы можете воспользоваться двумя различными способами:

  • Создайте командную строку для запуска через 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=
#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 либо пустым, либо "echo" в верхней части вашего скрипта, и он либо делает это, либо просто повторяет его.

Хороший вызов:) Это должно быть "легко", если у вас достаточно времени для поддержки 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
Другие вопросы по тегам