Назад | Перейти на главную страницу

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

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

Это нетривиальная проблема. 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