Mengutip dalam ssh $ host $ FOO dan ssh $ host tipe "sudo su user -c $ FOO"

30

Saya sering berakhir dengan mengeluarkan perintah kompleks melalui ssh; perintah ini melibatkan pemipaan ke awk atau perl satu-baris, dan sebagai hasilnya berisi tanda kutip tunggal dan $. Saya belum dapat menemukan aturan yang keras dan cepat untuk melakukan penawaran dengan benar, atau menemukan referensi yang baik untuk itu. Sebagai contoh, pertimbangkan hal berikut:

# what I'd run locally:
CMD='pgrep -fl java | grep -i datanode | awk '{print $1}'
# this works with ssh $host "$CMD":
CMD='pgrep -fl java | grep -i datanode | awk '"'"'{print $1}'"'"

(Catat kutipan tambahan dalam pernyataan awk.)

Tetapi bagaimana cara saya mengatasinya, misalnya ssh $host "sudo su user -c '$CMD'"? Apakah ada resep umum untuk mengelola penawaran dalam skenario seperti itu? ..

Leo Alekseyev
sumber

Jawaban:

35

Berurusan dengan beberapa tingkat pengutipan (sebenarnya, beberapa level penguraian / interpretasi) dapat menjadi rumit. Ini membantu untuk mengingat beberapa hal:

  • Setiap “level kutipan” berpotensi melibatkan bahasa yang berbeda.
  • Aturan mengutip bervariasi menurut bahasa.
  • Ketika berhadapan dengan lebih dari satu atau dua level bersarang, biasanya paling mudah untuk bekerja "dari bawah, atas" (yaitu yang paling dalam ke yang terluar).

Tingkat Pengutipan

Mari kita lihat contoh perintah Anda.

pgrep -fl java | grep -i datanode | awk '{print $1}'

Perintah contoh pertama Anda (di atas) menggunakan empat bahasa: shell Anda, regex di pgrep , regex di grep (yang mungkin berbeda dari bahasa regex di pgrep ), dan awk . Ada dua level interpretasi yang terlibat: shell dan satu level setelah shell untuk masing-masing perintah yang terlibat. Hanya ada satu tingkat kutipan yang eksplisit (shell quoting ke awk ).

ssh host 

Selanjutnya Anda menambahkan level ssh di atas. Ini secara efektif adalah level shell yang lain: ssh tidak menginterpretasikan perintah itu sendiri, ia memberikannya ke shell di ujung remote (via (eg) sh -c …) dan shell itu menginterpretasikan string.

ssh host "sudo su user -c …"

Kemudian Anda bertanya tentang menambahkan level shell lain di tengah dengan menggunakan su (via sudo , yang tidak menafsirkan argumen perintahnya, sehingga kita dapat mengabaikannya). Pada titik ini, Anda memiliki tiga level penumpukan yang terjadi ( awk → shell, shell → shell ( ssh ), shell → shell ( su user -c ), jadi saya menyarankan menggunakan pendekatan "bawah, atas". Saya akan berasumsi bahwa shell Anda kompatibel dengan Bourne (mis. sh , ash , dash , ksh , bash , zsh , dll.) Beberapa jenis shell lainnya ( ikan , rc, dll.) mungkin memerlukan sintaks yang berbeda, tetapi metode ini masih berlaku.

Bawah, Atas

  1. Merumuskan string yang ingin Anda wakili di tingkat terdalam.
  2. Pilih mekanisme penawaran dari daftar kutipan bahasa tertinggi berikutnya.
  3. Mengutip string yang diinginkan sesuai dengan mekanisme penawaran yang Anda pilih.
    • Sering ada banyak variasi cara menerapkan mekanisme penawaran mana. Melakukannya dengan tangan biasanya adalah soal latihan dan pengalaman. Ketika melakukannya secara terprogram, biasanya yang terbaik adalah memilih yang paling mudah untuk mendapatkan yang benar (biasanya yang "paling harfiah" (pelarian paling sedikit)).
  4. Secara opsional, gunakan string yang dikutip yang dihasilkan dengan kode tambahan.
  5. Jika Anda belum mencapai tingkat kutipan / interpretasi yang Anda inginkan, ambil string yang dikutip yang dihasilkan (ditambah kode tambahan apa pun) dan gunakan sebagai string awal pada langkah 2.

Mengutip Semantik Bervariasi

Yang perlu diingat di sini adalah bahwa setiap bahasa (level kutipan) dapat memberikan semantik yang sedikit berbeda (atau bahkan semantik yang berbeda secara drastis) dengan karakter kutipan yang sama.

Sebagian besar bahasa memiliki mekanisme kutipan "literal", tetapi mereka berbeda persis dalam arti literalnya. Kutipan tunggal kerang mirip Bourne sebenarnya literal (yang berarti Anda tidak dapat menggunakannya untuk mengutip karakter kutipan sendiri). Bahasa lain (Perl, Ruby) kurang literal karena mereka menginterpretasikan beberapa urutan backslash di dalam wilayah yang dikutip tunggal secara non-harfiah (khusus, \\dan \'menghasilkan \dan ', tetapi urutan backslash lainnya sebenarnya literal).

Anda harus membaca dokumentasi untuk masing-masing bahasa Anda untuk memahami aturan penawaran dan sintaksis keseluruhan.

Contoh Anda

Level terdalam dari contoh Anda adalah program awk .

{print $1}

Anda akan menanamkan ini di baris perintah shell:

pgrep -fl java | grep -i datanode | awk 

Kita perlu untuk melindungi (minimal) ruang dan $dalam awk Program. Pilihan yang jelas adalah menggunakan kutipan tunggal di shell di seluruh program.

  • '{print $1}'

Ada beberapa pilihan lain:

  • {print\ \$1} langsung melarikan diri dari ruang dan $
  • {print' $'1} kutipan tunggal hanya ruang dan $
  • "{print \$1}" dua kali lipat kutipan keseluruhan dan melarikan diri $
  • {print" $"1}Kutip ganda hanya spasi dan $
    Ini mungkin sedikit membengkokkan aturan (tidak dihapus $pada akhir string ganda dikutip adalah literal), tetapi tampaknya bekerja di sebagian besar shell.

Jika program menggunakan koma antara kurung kurawal buka dan tutup, kita juga perlu mengutip atau melarikan diri dari koma atau kurung kurawal untuk menghindari “penjepit ekspansi” di beberapa cangkang.

Kami mengambil '{print $1}'dan menanamkannya di sisa "kode" shell:

pgrep -fl java | grep -i datanode | awk '{print $1}'

Selanjutnya, Anda ingin menjalankan ini melalui su dan sudo .

sudo su user -c 

su user -c …sama seperti some-shell -c …(kecuali berjalan di bawah beberapa UID lainnya), jadi su hanya menambahkan level shell lain. sudo tidak menginterpretasikan argumennya, sehingga tidak menambahkan level kutipan.

Kita membutuhkan level shell lain untuk string perintah kita. Kita dapat memilih satu kutipan lagi, tetapi kita harus memberikan penanganan khusus pada kutipan tunggal yang ada. Cara yang biasa terlihat seperti ini:

'pgrep -fl java | grep -i datanode | awk '\''{print $1}'\'

Ada empat string di sini yang shell akan menafsirkan dan menyatukan: string dikutip tunggal pertama ( pgrep … awk), sebuah kutipan tunggal lolos, program awk dikutip tunggal , yang lain lolos kutipan tunggal.

Tentu saja ada banyak alternatif:

  • pgrep\ -fl\ java\ \|\ grep\ -i\ datanode\ \|\ awk\ \'{print\ \$1} lepaskan segala sesuatu yang penting
  • pgrep\ -fl\ java\|grep\ -i\ datanode\|awk\ \'{print\$1}sama, tetapi tanpa spasi berlebihan (bahkan dalam program awk !)
  • "pgrep -fl java | grep -i datanode | awk '{print \$1}'" gandakan kutipan semuanya, lepaskan $
  • 'pgrep -fl java | grep -i datanode | awk '"'"'{print \$1}'"'"variasi Anda; sedikit lebih lama dari cara biasa karena menggunakan tanda kutip ganda (dua karakter) alih-alih melarikan diri (satu karakter)

Menggunakan berbagai kutipan di tingkat pertama memungkinkan variasi lain di tingkat ini:

  • 'pgrep -fl java | grep -i datanode | awk "{print \$1}"'
  • 'pgrep -fl java | grep -i datanode | awk {print\ \$1}'

Menanamkan variasi pertama pada baris perintah sudo / * su * berikan ini:

sudo su user -c 'pgrep -fl java | grep -i datanode | awk '\''{print $1}'\'

Anda bisa menggunakan string yang sama dalam konteks level shell tunggal lainnya (mis ssh host ….).

Selanjutnya, Anda menambahkan level ssh di atas. Ini secara efektif adalah level shell yang lain: ssh tidak menginterpretasikan perintah itu sendiri, tetapi ia menyerahkannya ke shell di ujung remote (via (eg) sh -c …) dan shell itu menginterpretasikan string.

ssh host 

Prosesnya sama: ambil string, pilih metode kutip, gunakan, sematkan.

Menggunakan satu kutipan lagi:

'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

Sekarang ada sebelas string yang diinterpretasikan dan digabungkan 'sudo su user -c ':, kutip tunggal lolos,, kutip tunggal 'pgrep … awk 'lolos, lolos backslash, dua kutip tunggal lolos, program awk tunggal yang dikutip , satu kutip tunggal lolos, backslash melarikan diri, dan satu kutip tunggal lolos .

Bentuk akhir terlihat seperti ini:

ssh host 'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

Ini agak sulit untuk diketik dengan tangan, tetapi sifat literal dari kutipan tunggal shell memudahkan untuk mengotomatiskan sedikit variasi:

#!/bin/sh

sq() { # single quote for Bourne shell evaluation
    # Change ' to '\'' and wrap in single quotes.
    # If original starts/ends with a single quote, creates useless
    # (but harmless) '' at beginning/end of result.
    printf '%s\n' "$*" | sed -e "s/'/'\\\\''/g" -e 1s/^/\'/ -e \$s/\$/\'/
}

# Some shells (ksh, bash, zsh) can do something similar with %q, but
# the result may not be compatible with other shells (ksh uses $'...',
# but dash does not recognize it).
#
# sq() { printf %q "$*"; }

ap='{print $1}'
s1="pgrep -fl java | grep -i datanode | awk $(sq "$ap")"
s2="sudo su user -c $(sq "$s1")"

ssh host "$(sq "$s2")"
Chris Johnsen
sumber
5
Penjelasan hebat!
Gilles 'SO- stop being evil'
7

Lihat jawaban Chris Johnsen untuk penjelasan yang jelas dan mendalam dengan solusi umum. Saya akan memberikan beberapa tips tambahan yang membantu dalam beberapa keadaan umum.

Kutipan tunggal lolos dari semuanya kecuali satu kutipan. Jadi, jika Anda tahu nilai variabel tidak termasuk kutipan tunggal, Anda dapat menginterpolasinya dengan aman di antara kutipan tunggal dalam skrip shell.

su -c "grep '$pattern' /root/file"  # assuming there is no ' in $pattern

Jika shell lokal Anda adalah ksh93 atau zsh, Anda dapat mengatasi dengan tanda kutip tunggal dalam variabel dengan menulis ulang '\''. (Meskipun bash juga memiliki ${foo//pattern/replacement}konstruknya, penanganannya dengan tanda kutip tunggal tidak masuk akal bagi saya.)

su -c "grep '${pattern//'/'\''}' /root/file"  # if the outer shell is zsh
su -c "grep '${pattern//\'/\'\\\'\'}' /root/file"  # if the outer shell is ksh93

Kiat lain untuk menghindari keharusan berurusan dengan kutip bersarang adalah dengan mengirimkan string melalui variabel lingkungan sebanyak mungkin. Ssh dan sudo cenderung menjatuhkan sebagian besar variabel lingkungan, tetapi sering dikonfigurasi untuk membiarkannya LC_*, karena ini biasanya sangat penting untuk kegunaan (mereka berisi informasi lokal) dan jarang dianggap sensitif terhadap keamanan.

LC_CMD='what you would use locally' ssh $host 'sudo su user -c "$LC_CMD"'

Di sini, karena LC_CMDberisi potongan shell, itu harus diberikan secara harfiah ke shell paling dalam. Oleh karena itu variabel diperluas oleh shell tepat di atas. Kerang yang paling dalam tapi satu hanya terlihat"$LC_CMD" , dan yang paling dalam melihat perintah.

Metode serupa berguna untuk meneruskan data ke utilitas pemrosesan teks. Jika Anda menggunakan interpolasi shell, utilitas akan memperlakukan nilai variabel sebagai perintah, mis. Tidak sed "s/$pattern/$replacement/"akan berfungsi jika variabel berisi /. Jadi gunakan awk (bukan sed), dan baik -vopsi atau ENVIRONlariknya untuk meneruskan data dari shell (jika Anda melaluinya ENVIRON, ingatlah untuk mengekspor variabel).

awk -vpattern="$pattern" replacement="$replacement" '{gsub(pattern,replacement); print}'
Gilles 'SANGAT berhenti menjadi jahat'
sumber
2

Seperti yang dijelaskan Chris Johnson dengan sangat baik , Anda memiliki beberapa tingkat tipuan kutipan di sini; Anda menginstruksikan lokal Anda shelluntuk menginstruksikan remote shellmelalui sshitu harus menginstruksikan sudountuk menginstruksikan suuntuk menginstruksikan remote shelluntuk menjalankan pipa Anda pgrep -fl java | grep -i datanode | awk '{print $1}'sebagai user. Perintah semacam itu mengharuskan banyak yang membosankan \'"quote quoting"\'.

Jika Anda menerima saran saya, Anda akan melepaskan semua omong kosong dan melakukan:

% ssh $host <<REM=LOC_EXPANSION <<'REMOTE_CMD' |
> localhost_data='$(commands run on localhost at runtime)' #quotes don't affect expansion
> more_localhost_data="$(values set at heredoc expansion)" #remote shell will receive m_h_d="result"
> REM=LOC_EXPANSION
> commands typed exactly as if located at 
> the remote terminal including variable 
> "${{more_,}localhost_data}" operations
> 'quotes and' \all possibly even 
> a\wk <<'REMOTELY_INVOKED_HEREDOC' |
> {as is often useful with $awk
> so long as the terminator for}
> REMOTELY_INVOKED_HEREDOC
> differs from that of REM=LOC_EXPANSION and
> REMOTE_CMD
> and here you can | pipeline operate on |\
> any output | received from | ssh as |\
> run above | in your local | terminal |\
> however | tee > ./you_wish.result
<desired output>

UNTUK LEBIH:

Periksa jawaban saya (mungkin terlalu panjang lebar) untuk jalur perpipaan dengan berbagai jenis kutipan untuk substitusi garis miring di mana saya membahas beberapa teori di balik mengapa itu bekerja.

-Mike

mikeserv
sumber
Ini terlihat menarik, tetapi saya tidak bisa membuatnya bekerja. Bisakah Anda memposting contoh kerja minimal?
John Lawrence Aspden
Saya percaya contoh ini memerlukan zsh karena menggunakan beberapa pengalihan ke stdin. Dalam cangkang mirip Bourne lainnya, yang kedua <<hanya menggantikan yang pertama. Haruskah ia mengatakan "hanya zsh" di suatu tempat, atau apakah saya melewatkan sesuatu? (Trik cerdik, untuk memiliki heredoc yang sebagian tunduk pada ekspansi lokal)
Berikut ini adalah versi yang kompatibel dengan bash: unix.stackexchange.com/questions/422489/...
dabest1
0

Bagaimana dengan menggunakan lebih banyak tanda kutip ganda?

Maka Anda ssh $host $CMDharus bekerja dengan baik dengan yang ini:

CMD="pgrep -fl java | grep -i datanode | awk '{print $1}'"

Sekarang untuk yang lebih kompleks, the ssh $host "sudo su user -c \"$CMD\"". Saya kira yang harus Anda lakukan adalah melarikan diri karakter sensitif di CMD: $, \dan ". Jadi saya akan mencoba dan melihat apakah ini berfungsi:echo $CMD | sed -e 's/[$\\"]/\\\1/g' .

Jika itu terlihat OK, bungkus echo + sed ke dalam fungsi shell, dan Anda bisa menggunakannya ssh $host "sudo su user -c \"$(escape_my_var $CMD)\"".

alex
sumber