Bash: Kutipan dilucuti ketika perintah dilewatkan sebagai argumen untuk suatu fungsi

8

Saya mencoba untuk menerapkan jenis mekanisme run kering untuk skrip saya dan menghadapi masalah kutipan yang dilucuti ketika perintah dilewatkan sebagai argumen ke fungsi dan mengakibatkan perilaku yang tidak terduga.

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 adalah:

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

Diharapkan:

su - webuser1 -c "cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' [email protected]"

Dengan printf diaktifkan alih-alih gema:

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

Hasil:

su: invalid option -- 1

Seharusnya tidak demikian jika kutipan tetap di tempat mereka dimasukkan. Saya juga sudah mencoba menggunakan "eval", tidak banyak perbedaan. Jika saya menghapus panggilan dry_run di email_admin dan kemudian menjalankan skrip, itu berfungsi dengan baik.

Shoaibi
sumber

Jawaban:

5

Coba gunakan \"bukan hanya ".

James
sumber
4

"$@"harus bekerja. Sebenarnya ini bekerja untuk saya dalam test case sederhana ini:

dry_run()
{
    "$@"
}

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

email_admin

Keluaran:

./foo.sh 
a
b

Diedit untuk menambahkan: output echo $@sudah benar. Ini "adalah meta-karakter dan bukan bagian dari parameter. Anda dapat membuktikan bahwa itu benar bekerja dengan menambahkan echo $5ke dry_run(). Ini akan menampilkan semuanya setelah-c

Mark Wagner
sumber
4

Ini bukan masalah sepele. Shell melakukan penghapusan kutipan sebelum memanggil fungsi, jadi tidak mungkin fungsi tersebut dapat membuat ulang kutipan persis seperti yang Anda ketik.

Namun, jika Anda hanya ingin dapat mencetak string yang dapat disalin dan ditempelkan untuk mengulangi perintah, ada dua pendekatan yang dapat Anda ambil:

  • Bangun string perintah untuk dijalankan melalui evaldan meneruskan string itu kedry_run
  • Kutip karakter khusus perintah dry_runsebelum dicetak

Menggunakan eval

Inilah cara yang dapat Anda gunakan evaluntuk mencetak apa yang dijalankan:

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

Keluaran:

su - webuser1  -c "cd /home/webuser1/public_html && git log -1 -p|mail -s 'Git deployment on webuser1' [email protected]"

Catat jumlah kutipan yang gila - Anda mendapat perintah di dalam perintah di dalam perintah, yang menjadi jelek dengan cepat. Hati-hati: Kode di atas akan bermasalah jika variabel Anda berisi spasi putih atau karakter khusus (seperti tanda kutip).

Mengutip Karakter Khusus

Pendekatan ini memungkinkan Anda untuk menulis kode lebih alami, tetapi hasilnya lebih sulit bagi manusia untuk dibaca karena cara cepat dan kotor shell_quotediterapkan:

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

Keluaran:

'su' '-' 'webuser1' '-c' 'cd /home/webuser1/public_html && git log -1 -p|mail -s '\''Git deployment on webuser1'\'' [email protected]'

Anda dapat meningkatkan keterbacaan output dengan mengubah shell_quoteke karakter khusus backslash-escape alih-alih membungkus segala sesuatu dalam tanda kutip tunggal, tetapi sulit dilakukan dengan benar.

Jika Anda melakukan shell_quotependekatan, Anda dapat membuat perintah untuk meneruskan dengan sucara yang lebih aman. Berikut ini akan berfungsi bahkan jika ${GIT_WORK_TREE},, ${mail_subject}atau ${admin_email}berisi karakter khusus (tanda kutip tunggal, spasi, tanda bintang, titik koma, dll.):

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

Keluaran:

'su' '-' 'webuser1' '-c' ''\''cd'\'' '\''/home/webuser1/public_html'\'' && git log -1 -p | '\''mail'\'' '\''-s'\'' '\''Git deployment on webuser1'\'' '\''[email protected]'\'''
Richard Hansen
sumber
2

Itu sulit, Anda dapat mencoba pendekatan lain yang saya lihat:

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

dengan cara itu Anda hanya mengatur DRY_RUN menjadi kosong atau "bergema" di bagian atas skrip Anda dan apakah itu atau hanya gema itu.

Steve Kehlet
sumber
0

Tantangan yang bagus :) Seharusnya "mudah" jika Anda memiliki bash yang cukup baru untuk mendukung $LINENOdan$BASH_SOURCE

Ini adalah upaya pertama saya, berharap ini sesuai dengan kebutuhan Anda:

#!/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
Olivier Dulac
sumber