Ganti beberapa string dalam sekali operan

11

Saya mencari cara untuk mengganti string placeholder dalam file template dengan nilai konkret, dengan alat Unix umum (bash, sed, awk, mungkin perl). Penting bahwa penggantian dilakukan dalam sekali jalan, yaitu, apa yang sudah dipindai / diganti tidak harus dipertimbangkan untuk penggantian lain. Misalnya, kedua upaya ini gagal:

echo "AB" | awk '{gsub("A","B");gsub("B","A");print}'
>> AA

echo "AB" | sed 's/A/B/g;s/B/A/g'
>> AA

Hasil yang benar dalam kasus ini tentu saja adalah BA.

Secara umum, solusinya harus sama dengan memindai input kiri-ke-kanan untuk kecocokan terpanjang ke salah satu string pengganti yang diberikan, dan untuk setiap kecocokan, melakukan penggantian dan melanjutkan dari titik itu pada input (tidak ada sudah membaca input atau penggantian yang dilakukan harus dipertimbangkan untuk pertandingan). Sebenarnya, detailnya tidak masalah, hanya saja hasil penggantian tidak pernah dipertimbangkan untuk penggantian lain, seluruhnya atau sebagian.

CATATAN Saya hanya mencari solusi generik yang benar. Tolong jangan mengusulkan solusi yang gagal untuk input tertentu (input file, cari dan ganti pasangan), namun tampaknya tidak mungkin.

Ambroz Bizjak
sumber
Saya menganggap mereka lebih dari satu karakter? Untuk ini, Anda bisa menggunakan tr AB BA.
Kevin
3
Dan sejujurnya, saya tidak akan terkejut jika seseorang menganggap catatan Anda agak kasar.
peterph
1
Bagaimana Anda berharap untuk "hanya mendapatkan solusi yang benar" ketika Anda belum memberikan input atau output sampel?
jasonwryan
1
Saya khawatir Anda harus melakukannya persis seperti yang Anda gambarkan - parsing dari awal dan gantikan saat Anda pergi - yaitu tidak dengan ekspresi reguler.
peterph
2
Ini adalah pertanyaan yang wajar, tetapi jawabannya adalah Anda memerlukan parser mesin negara , yang merupakan apa yang diberikan oleh rici (dalam gaya hacker sejati, saya pikir). Dengan kata lain, Anda meremehkan kompleksitas tugas, ala "Saya ingin mengurai secara umum (HT | X) ML dengan ekspresi reguler" -> Jawabannya adalah TIDAK. Anda tidak dapat (hanya) menggunakan sed. Anda tidak dapat (hanya) menggunakan awk. AFAIK tidak ada alat yang ada yang akan melakukan ini di luar kotak. Eksploitasi Sans rici, Anda harus menulis beberapa kode.
goldilocks

Jawaban:

10

OK, solusi umum. Fungsi bash berikut membutuhkan 2kargumen; masing-masing pasangan terdiri dari pengganti dan satu pengganti. Terserah Anda untuk mengutip string dengan tepat untuk meneruskannya ke dalam fungsi. Jika jumlah argumen aneh, argumen kosong implisit akan ditambahkan, yang secara efektif akan menghapus kejadian placeholder terakhir.

Baik penampung atau pengganti tidak boleh mengandung karakter NUL, tetapi Anda dapat menggunakan standar C \-escapes seperti \0jika Anda membutuhkan NUL(dan akibatnya Anda harus menulis \\jika Anda menginginkan a \).

Ini membutuhkan alat bantu standar yang harus ada pada sistem seperti posix (lex dan cc).

replaceholder() {
  local dir=$(mktemp -d)
  ( cd "$dir"
    { printf %s\\n "%option 8bit noyywrap nounput" "%%"
      printf '"%s" {fputs("%s", yyout);}\n' "${@//\"/\\\"}"
      printf %s\\n "%%" "int main(int argc, char** argv) { return yylex(); }"
    } | lex && cc lex.yy.c
  ) && "$dir"/a.out
  rm -fR "$dir"
}

Kami berasumsi bahwa \sudah lolos jika perlu dalam argumen tetapi kami harus lolos dari tanda kutip ganda, jika ada. Itulah yang dilakukan argumen kedua ke printf kedua. Karena lextindakan defaultnya adalah ECHO, kita tidak perlu khawatir tentang hal itu.

Contoh menjalankan (dengan timing untuk skeptis; itu hanya laptop komoditas murah):

$ time echo AB | replaceholder A B B A
BA

real    0m0.128s
user    0m0.106s
sys     0m0.042s
$ time printf %s\\n AB{0000..9999} | replaceholder A B B A > /dev/null

real    0m0.118s
user    0m0.117s
sys     0m0.043s

Untuk input yang lebih besar, mungkin berguna untuk menyediakan flag optimasi cc, dan untuk kompatibilitas Posix saat ini, akan lebih baik untuk digunakan c99. Implementasi yang bahkan lebih ambisius mungkin mencoba untuk membuat cache executable yang dihasilkan alih-alih menghasilkan mereka setiap kali, tetapi mereka tidak benar-benar mahal untuk dihasilkan.

Edit

Jika Anda memiliki tcc , Anda dapat menghindari kerumitan membuat direktori sementara, dan menikmati waktu kompilasi yang lebih cepat yang akan membantu pada input berukuran normal:

treplaceholder () { 
  tcc -run <(
  {
    printf %s\\n "%option 8bit noyywrap nounput" "%%"
    printf '"%s" {fputs("%s", yyout);}\n' "${@//\"/\\\"}"
    printf %s\\n "%%" "int main(int argc, char** argv) { return yylex(); }"
  } | lex -t)
}

$ time printf %s\\n AB{0000..9999} | treplaceholder A B B A > /dev/null

real    0m0.039s
user    0m0.041s
sys     0m0.031s
rici
sumber
Saya tidak yakin apakah ini lelucon atau tidak;)
Ambroz Bizjak
3
@ambrozbizjak: Berhasil, cepat untuk input besar dan cepat untuk input kecil. Mungkin tidak menggunakan alat yang Anda pikirkan tetapi mereka adalah alat standar. Mengapa ini menjadi lelucon?
rici
4
+1 Karena tidak menjadi lelucon! : D
goldilocks
Itu akan seperti POSIX portabel fn() { tcc ; } <<CODE\n$(gen code)\nCODE\n. Bisakah saya bertanya - ini jawaban yang luar biasa dan saya memutarnya segera setelah saya membacanya - tapi saya tidak mengerti apa yang terjadi pada susunan shell? Apa fungsinya "${@//\"/\\\"}"?
mikeserv
@mikeserv: «Untuk setiap argumen sebagai nilai yang dikutip (" $ @ "), ganti semua (//) kemunculan kutipan (\") dengan (/) garis miring terbalik (\\) diikuti dengan kutipan (\ ") ». Lihat Ekspansi parameter di manual bash.
rici
1
printf 'STRING1STRING1\n\nSTRING2STRING1\nSTRING2\n' |
od -A n -t c -v -w1 |
sed 's/ \{1,3\}//;s/\\$/&&/;H;s/.*//;x
     /\nS\nT\nR\nI\nN\nG\n1/s//STRING2/
     /\nS\nT\nR\nI\nN\nG\n2/s//STRING1/
     /\\n/!{x;d};s/\n//g;s/./\\&/g' |
     xargs printf %b

###OUTPUT###

STRING2STRING2

STRING1STRING2
STRING1

Sesuatu seperti ini akan selalu mengganti setiap kemunculan string target Anda hanya sekali saat mereka terjadi seddalam aliran dengan satu gigitan per baris. Ini adalah cara tercepat yang dapat saya bayangkan Anda akan melakukannya. Kemudian lagi, saya tidak menulis C. Tapi ini benar-benar menangani pembatas nol jika Anda menginginkannya. Lihat jawaban ini untuk cara kerjanya. Ini tidak memiliki masalah dengan karakter shell khusus yang terkandung atau serupa - tetapi itu adalah ASCII spesifik lokal, atau, dengan kata lain, odtidak akan menampilkan karakter multi-byte pada baris yang sama dan hanya akan melakukan satu per. Jika ini masalah, Anda ingin menambahkan iconv.

mikeserv
sumber
+1 Mengapa Anda mengatakan itu hanya menggantikan "kejadian paling awal dari string target Anda"? Dalam output sepertinya akan menggantikan semuanya. Saya tidak meminta untuk melihatnya, tetapi dapatkah ini dilakukan tanpa hardcoding nilai-nilai?
goldilocks
@goldilocks - Ya - tetapi hanya segera setelah itu terjadi. Mungkin saya harus menulis ulang itu. Dan yeah - Anda bisa menambahkan tengah seddan menyimpan hingga nol atau sesuatu lalu minta yang sedmenulis skrip ini; atau letakkan di fungsi shell dan berikan nilai pada satu gigitan per baris seperti "/$1/"... "/$2/"- mungkin saya akan menulis fungsi-fungsi itu juga ...
mikeserv
Ini sepertinya tidak berfungsi dalam kasus di mana placeholder berada PLACE1, PLACE2dan PLA. PLAselalu menang. OP mengatakan: "setara dengan memindai input kiri-ke-kanan untuk pertandingan terlama dengan salah satu string pengganti yang diberikan" (penekanan ditambahkan)
rici
@rici - terima kasih. Maka saya harus melakukan pembatas nol. Kembali dalam sekejap.
mikeserv
@rici - Saya baru saja akan memposting versi lain, yang akan menangani apa yang Anda gambarkan, tetapi melihatnya lagi dan saya rasa saya tidak harus melakukannya. Katanya terpanjang untuk salah satu string pengganti yang diberikan. Ini melakukan itu. Tidak ada indikasi bahwa satu string adalah subset dari yang lain, hanya saja nilai yang diganti mungkin. Saya juga tidak berpikir iterasi daftar adalah cara yang sah untuk menyelesaikan masalah. Mengingat masalah seperti yang saya mengerti, ini adalah solusi yang berfungsi.
mikeserv
1

Sebuah perlsolusi. Bahkan jika beberapa menyatakan itu tidak mungkin, saya menemukan satu tetapi secara umum pertandingan sederhana dan ganti tidak mungkin dan bahkan semakin buruk karena pengulangan NFA hasilnya bisa tidak terduga.

Secara umum, dan ini harus dikatakan, masalahnya menghasilkan hasil yang berbeda yang tergantung pada urutan dan panjang tupel pengganti. yaitu:

A B
AA CC

dan input AAAmenghasilkan BBBatau CCB.

Berikut kodenya:

#!/usr/bin/perl

$v='if (0) {} ';
while (($a,$b)=split /\s+/, <DATA>) {
  $k.=$a.'|';
  $v.='elsif ($& eq \''.$a.'\') {print \''.$b.'\'} ';
}
$k.='.';
$v.='else {print $&;}';

eval "
while (<>) {
  \$_ =~ s/($k)/{$v}/geco;
}";  
print "\n";


__DATA__
A    B
B    A
abba baab
baab abbc
abbc aaba

Checkerbunny:

$ echo 'ABBabbaBBbaabAAabbc'|perl script
$ BAAbaabAAabbcBBaaba

sumber