cara terpendek untuk mengganti karakter dalam suatu variabel

18

Ada banyak cara untuk mengganti karakter dalam suatu variabel.

Cara terpendek yang saya temukan trsejauh ini:

OUTPUT=a\'b\"c\`d_123and_a_lot_more
OUTPUT=$(echo "$OUTPUT"|tr -d "'\`\"")
echo $OUTPUT

Apakah ada cara yang lebih cepat? Dan apakah kutipan ini aman untuk kutipan seperti ', "dan `sendiri?

rubo77
sumber
Saya pikir Anda dapat terus menggunakan tr. BASH's PE bagus tetapi tr jauh lebih cepat dalam kasus ini. mis. echo "$OUTPUT" | tr -dc '[[:alpha:]]' karena Anda hanya ingin memiliki alfanumerik
Valentin Bajrami
2
Karena Anda tertarik untuk menjadi ahli dalam mengutip: Selalu kutip variabel Anda! echo "$OUTPUT". Atau lebih baik: printf "%s\n" "$OUTPUT". (Apa yang terjadi kapan OUTPUT="-n"?)
musiphil
Anda juga dapat mempertimbangkan menjelajah codegolf , terutama tip bash .
hoosierEE

Jawaban:

22

Ayo lihat. Yang terpendek yang bisa saya kemukakan adalah tweak dari trsolusi Anda :

OUTPUT="$(tr -d "\"\`'" <<<$OUTPUT)"

Alternatif lain termasuk substitusi variabel yang sudah disebutkan yang bisa lebih pendek dari yang ditunjukkan sejauh ini:

OUTPUT="${OUTPUT//[\'\"\`]}"

Dan sedtentu saja meskipun ini lebih panjang dalam hal karakter:

OUTPUT="$(sed s/[\'\"\`]//g <<<$OUTPUT)"

Saya tidak yakin apakah maksud Anda pendek atau dalam waktu yang singkat. Dalam hal panjang, keduanya pendek seperti yang didapat (atau seperti yang bisa saya dapatkan) ketika harus menghapus karakter tertentu. Jadi, mana yang tercepat? Saya menguji dengan menetapkan OUTPUTvariabel ke apa yang Anda miliki dalam contoh Anda tetapi diulang beberapa lusin kali:

$ echo ${#OUTPUT} 
4900

$ time tr -d "\"\`'" <<<$OUTPUT
real    0m0.002s
user    0m0.004s
sys     0m0.000s
$ time sed s/[\'\"\`]//g <<<$OUTPUT
real    0m0.005s
user    0m0.000s
sys     0m0.000s
$ time echo ${OUTPUT//[\'\"\`]}
real    0m0.027s
user    0m0.028s
sys     0m0.000s

Seperti yang Anda lihat, trini jelas yang tercepat, diikuti oleh sed. Juga, sepertinya menggunakan echosebenarnya sedikit lebih cepat daripada menggunakan <<<:

$ for i in {1..10}; do 
    ( time echo $OUTPUT | tr -d "\"\`'" > /dev/null ) 2>&1
done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0025
$ for i in {1..10}; do 
    ( time tr -d "\"\`'" <<<$OUTPUT > /dev/null ) 2>&1 
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0029

Karena perbedaannya kecil, saya menjalankan tes di atas 10 kali untuk masing-masing dari keduanya dan ternyata yang tercepat memang yang Anda harus mulai dengan:

echo $OUTPUT | tr -d "\"\`'" 

Namun, ini berubah ketika Anda memperhitungkan overhead penetapan ke variabel, di sini, menggunakan trsedikit lebih lambat daripada penggantian sederhana:

$ for i in {1..10}; do
    ( time OUTPUT=${OUTPUT//[\'\"\`]} ) 2>&1
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0032

$ for i in {1..10}; do
    ( time OUTPUT=$(echo $OUTPUT | tr -d "\"\`'")) 2>&1
  done | grep -oP 'real.*m\K[\d.]+' | awk '{k+=$1;} END{print k/NR}'; 
0.0044

Jadi, sebagai kesimpulan, ketika Anda hanya ingin melihat hasilnya, gunakan trtetapi jika Anda ingin menetapkan kembali ke variabel, menggunakan fitur manipulasi string shell lebih cepat karena mereka menghindari overhead menjalankan subkulit terpisah.

terdon
sumber
4
Karena OP tertarik untuk mengembalikan nilai yang dimodifikasi OUTPUT, Anda harus memperhitungkan penggantian overhead sub-shell substitusi perintah yang terlibat trdan sedsolusinya
iruvar
@ 1_CR ya tapi karena itu akan menjadi kasus metode mana pun yang dia gunakan, saya pikir itu tidak relevan.
terdon
1
Tidak cukup, OUTPUT="${OUTPUT//[`\"\']/}" tidak melibatkan penggantian perintah
iruvar
@ 1_CR ah, saya mengerti, ya, Anda benar dan itu memang mengubah hasilnya. Terima kasih, jawabannya diedit.
terdon
2
Metode yang melibatkan substitusi perintah memiliki kelemahan yaitu agak membingungkan string. (Anda dapat menghindarinya tetapi dengan mengorbankan membuat perintah secara signifikan lebih kompleks.) Khususnya, penggantian perintah menghilangkan baris baru yang tertinggal.
Gilles 'SO- stop being evil'
15

Anda bisa menggunakan substitusi variabel :

$ OUTPUT=a\'b\"c\`d
$ echo "$OUTPUT"
a'b"c`d

Gunakan sintaksis itu: ${parameter//pattern/string}untuk mengganti semua kemunculan pola dengan string.

$ echo "${OUTPUT//\'/x}"
axb"c`d
$ echo "${OUTPUT//\"/x}"
a'bxc`d
$ echo "${OUTPUT//\`/x}"
a'b"cxd
$ echo "${OUTPUT//[\'\"\`]/x}"
axbxcxd
kekacauan
sumber
@ rubo77 echo ${OUTPUT//[`\"\']/x}memberiaxbxcxa
kekacauan
Tidak benar menyebut ekspansi "ekspansi variabel". Ini disebut "ekspansi parameter".
gena2x
@ gena2x - Saya tidak mengerti apa artinya komentar Anda di sini?
slm
12

Dalam bash atau zsh itu adalah:

OUTPUT="${OUTPUT//[\`\"\']/}"

Perhatikan bahwa ${VAR//PATTERN/}menghapus semua instance dari pola. Untuk informasi lebih lanjut, ekspansi parameter bash

Solusi ini harus tercepat untuk string pendek karena tidak melibatkan menjalankan program eksternal apa pun. Namun untuk string yang sangat panjang kebalikannya benar - lebih baik menggunakan alat khusus untuk operasi teks, misalnya:

$ OUTPUT="$(cat /usr/src/linux/.config)"

$ time (echo $OUTPUT | OUTPUT="${OUTPUT//set/abc}")
real    0m1.766s
user    0m1.681s
sys     0m0.002s

$ time (echo $OUTPUT | sed s/set/abc/g >/dev/null)
real    0m0.094s
user    0m0.078s
sys     0m0.006s
gena2x
sumber
1
Bahkan, trlebih cepat. Regex dan gumpalan mahal, dan sementara tidak ada program eksternal di sini, bash akan selalu lebih lambat daripada sesuatu seperti tr.
terdon
Itu sangat tergantung pada input data dan pada implementasi regexp. Dalam jawaban Anda, Anda mengambil beberapa kumpulan data besar spesifik - tetapi kumpulan data mungkin kecil. Atau berbeda. Juga, Anda mengukur bukan waktu regexp tetapi waktu gema, jadi saya tidak bisa memastikan apakah perbandingan Anda benar-benar adil.
gena2x
Poin bagus. Namun, Anda tidak dapat membuat klaim tentang kecepatan tanpa pengujian. Bahkan, ketika menugaskan ke variabel ini tampaknya lebih cepat tetapi ketika mencetak ke layar trmenang (lihat jawaban saya). Saya setuju bahwa itu akan tergantung pada banyak faktor tetapi itulah mengapa Anda tidak dapat menentukan mana yang menang tanpa benar-benar mengujinya.
terdon
6

Jika, jika tidak sengaja, Anda hanya mencoba menangani tanda kutip untuk menggunakan kembali shell, maka Anda dapat melakukan ini tanpa menghapusnya, dan itu juga sederhana:

aq() { sh -c 'for a do
       alias "$((i=$i+1))=$a"
       done; alias' -- "$@"
}

Shell fungsi itu mengutip setiap argumen arg yang Anda berikan dan meningkatkan outputnya per argumen yang dapat diperbaiki.

Ini dia dengan beberapa argumen:

aq \
"here's an
ugly one" \
"this one is \$PATHpretty bad, too" \
'this one```****```; totally sucks'

KELUARAN

1='here'"'"'s an
ugly one'
2='this one is $PATHpretty bad, too'
3='this one```****```; totally sucks'

Keluaran itu dari dashmana biasanya kutipan dengan kutip tunggal berupa keluaran yang disukai '"'"'.bashakan lakukan '\''.

Mengganti pilihan byte tunggal, non-spasi putih, non-nol dengan byte tunggal lainnya mungkin dapat dilakukan paling cepat di setiap shell POSIX dengan $IFSdan $*.

set -f; IFS=\"\'\`; set -- $var; printf %s "$*"

KELUARAN

"some ""crazy """"""""string ""here

Di sana saya hanya printfagar Anda dapat melihatnya, tetapi tentu saja, jika saya telah melakukannya:

var="$*"

... daripada printfperintah$var akan menjadi apa yang Anda lihat di output di sana.

Ketika saya set -fmemerintahkan shell untuk tidak glob - jika string berisi karakter yang dapat ditafsirkan sebagai pola glob. Saya melakukan ini karena parser shell memperluas pola glob setelah melakukan pemisahan bidang pada variabel. globbing dapat diaktifkan kembali seperti set +f. Secara umum - dalam skrip - saya merasa berguna untuk mengatur bang saya seperti:

#!/usr/bin/sh -f

Dan kemudian secara eksplisit mengaktifkan globbing dengan set +fapa pun garis yang saya inginkan.

Pemecahan bidang terjadi berdasarkan karakter dalam $IFS.

Ada dua jenis $IFSnilai - $IFSspasi putih dan $IFSnon-spasi putih. bidang terbatas $IFSspasi (spasi, tab, baris baru) ditetapkan untuk dihapus oleh urutan ke satu bidang (atau tidak sama sekali jika tidak mendahului hal lain) - jadi ...

IFS=\ ; var='      '; printf '<%s>' $var
<>

Tetapi semua yang lain ditentukan untuk mengevaluasi ke satu bidang per kejadian - mereka tidak terpotong.

IFS=/; var='/////'; printf '<%s>' $var
<><><><><>

Semua ekspansi variabel, secara default, $IFSarray data dibatasi - mereka dibagi ke bidang yang terpisah sesuai dengan $IFS. Ketika Anda "-quote satu Anda menimpa properti array itu dan mengevaluasinya sebagai string tunggal.

Jadi ketika saya melakukannya ...

IFS=\"\'\`; set -- $var

Saya mengatur array argumen shell ke banyak $IFSbidang terbatas yang dihasilkan oleh $varekspansi. Ketika diperluas nilai konstituen untuk karakter yang terkandung dalam $IFSyang hilang - mereka hanya pemisah lapangan sekarang - mereka \0NUL.

"$*"- seperti ekspansi variabel ganda yang dikutip ganda - juga mengesampingkan kualitas pemisahan bidang dari $IFS. Tetapi, di samping itu , ia menggantikan byte pertama $IFS untuk setiap bidang yang dibatasi di "$@". Jadi karena "merupakan pertama nilai dalam $IFS semua pembatas berikutnya menjadi "di "$*". Dan yang "tidak perlu ada di $IFSsaat Anda membaginya, juga. Anda bisa mengubah $IFS setelah set -- $args ke nilai lain seluruhnya dan byte pertama yang baru kemudian akan muncul untuk pembatas bidang di "$*". Terlebih lagi, Anda dapat menghapus semua jejak mereka sepenuhnya seperti:

set -- $var; IFS=; printf %s "$*"

KELUARAN

some crazy string here
mikeserv
sumber
Sangat bagus, +1. Saya bertanya-tanya apakah ini memang lebih cepat. Bisakah Anda menambahkan beberapa tes waktu membandingkannya dengan pendekatan dalam jawaban saya? Saya berharap Anda akan lebih cepat tetapi ingin melihat.
terdon
@terdon - itu tergantung pada shell. Ini hampir pasti lebih cepat daripada trdi shell mana pun, tetapi perbedaannya rapuh bashuntuk ${var//$c/$newc/}kasus ini. Saya berharap bahkan dalam kasus itu akan lebih cepat dengan margin tertentu, tetapi saya biasanya tidak khawatir tentang itu karena untuk hal ini saya selalu menggunakan dash- yang lebih cepat dengan perintah besarnya pada umumnya dalam segala hal. Dan sulit untuk membandingkan.
mikeserv
@terdon - Saya mencoba. Tetapi - bahkan dalam bashmelakukan time (IFS=\"\'`; set -- $var; printf %s "$*")dan time (var=${var//\'`/\"/})keduanya menghasilkan 0.0000shasil untuk semua bidang. Apakah saya melakukan sesuatu yang salah, menurut Anda? Seharusnya ada backslash sebelum backquote di sana, tetapi saya tidak tahu bagaimana menempatkan backquote di bidang kode komentar.
mikeserv