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

8

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

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

Shoaibi
источник

Ответы:

5

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

Джеймс
источник
4

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

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

Марк Вагнер
источник
4

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

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

  • Создайте командную строку для запуска 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'\'''
Ричард Хансен
источник
2

Это сложно, вы можете попробовать другой подход, который я видел:

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" в верхней части вашего скрипта, и он либо делает это, либо просто повторяет его.

Стив Келет
источник
0

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