I am trying to implement a dry run kind of mechanism for my script and facing the issue of quotes getting stripped off when a command is passed as an argument to a function and resulting in unexpected behavior.

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"
    }

Output is:

su - webuser1 -c cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' user@domain.com

Expected:

su - webuser1 -c "cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' user@domain.com"

With printf enabled instead of echo:

su - webuser1 -c cd\ /home/webuser1/public_html\ \&\&\ git\ log\ -1\ -p\|mail\ -s\ \'Git\ deployment\ on\ webuser1\'\ user@domain.com

Result:

su: invalid option -- 1

That shouldn't be the case if quotes remained where they were inserted. I have also tried using "eval", not much difference. If i remove the dry_run call in email_admin and then run script, it work great.

link|improve this question

48% accept rate
feedback

4 Answers

"$@" should work. In fact it works for me in this simple test case:

dry_run()
{
    "$@"
}

email_admin()
{
    dry_run su - foo -c "cd /var/tmp && ls -1"
}

email_admin

Output:

./foo.sh 
a
b

Edited to add: the output of echo $@ is correct. The " is a meta-character and not part of the parameter. You can prove that it is correctly working by adding echo $5 to dry_run(). It will output everything after -c

link|improve this answer
feedback

This is not a trivial problem. Shell performs quote removal before calling the function, so there's no way the function can recreate the quotes exactly as you typed them.

However, if you just want to be able to print out a string that can be copied and pasted to repeat the command, there are two different approaches you can take:

  • Build a command string to be run via eval and pass that string to dry_run
  • Quote the command's special characters in dry_run before printing

Using eval

Here's how you could use eval to print exactly what is run:

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"
}

Output:

su - webuser1  -c "cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' user@domain.com"

Note the crazy amount of quoting -- you've got a command within a command within a command, which gets ugly quickly. Beware: The above code will have problems if your variables contain whitespace or special characters (like quotes).

Quoting Special Characters

This approach enables you to write code more naturally, but the output is harder for humans to read because of the quick-and-dirty way shell_quote is implemented:

# 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"
}

Output:

'su' '-' 'webuser1' '-c' 'cd /home/webuser1/public_html && git log -1 -p|mail -s '\''Git deployment on webuser1'\'' user@domain.com'

You can improve the readability of the output by changing shell_quote to backslash-escape special characters instead of wrapping everything in single quotes, but it's hard to do correctly.

If you do the shell_quote approach, you can construct the command to pass to su in a safer way. The following would work even if ${GIT_WORK_TREE}, ${mail_subject}, or ${admin_email} contained special characters (single quotes, spaces, asterisks, semicolons, 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"
}

Output:

'su' '-' 'webuser1' '-c' ''\''cd'\'' '\''/home/webuser1/public_html'\'' && git log -1 -p | '\''mail'\'' '\''-s'\'' '\''Git deployment on webuser1'\'' '\''user@domain.com'\'''
link|improve this answer
feedback

Try using \" instead of just ".

link|improve this answer
feedback

That's tricky, you might try this other approach I've seen:

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"
    }

that way you just set DRY_RUN to either blank or "echo" at the top of your script and it either does it or just echoes it.

link|improve this answer
feedback

Your Answer

 
or
required, but never shown

Not the answer you're looking for? Browse other questions tagged or ask your own question.