Bagaimana cara melakukan penggantian teks dalam hierarki folder besar?

11

Saya ingin mencari dan mengganti beberapa teks dalam satu set besar file tidak termasuk beberapa contoh. Untuk setiap baris, saya ingin meminta menanyakan apakah saya perlu mengganti baris itu atau tidak. Sesuatu yang mirip dengan vim :%s/from/to/gc(dengan cuntuk meminta konfirmasi), tetapi di seluruh set folder. Apakah ada alat atau skrip baris perintah yang baik yang dapat digunakan?

balki
sumber
Tentang pentingnya pemformatan yang tepat: Saya awalnya membaca perintah Anda s/from/to/gdengan kesalahan pemformatan setelahnya, bukan s/from/to/gcdengan penekanan pada csaat Anda berusaha menulis (Anda tidak dapat melakukannya dengan Markdown, Anda bisa melakukannya dengan <code>dan <strong>tag HTML).
Gilles 'SANGAT berhenti menjadi jahat'

Jawaban:

19

Mengapa tidak menggunakan vim?

Buka semua file dalam vim

vim $(find . -type f)

Atau hanya buka file yang relevan (seperti yang disarankan oleh Caleb)

vim $(grep 'from' . -Rl)

Dan kemudian jalankan ganti di semua buffer

:bufdo %s/from/to/gc | update

Anda juga dapat melakukannya dengan sed, tetapi pengetahuan saya terbatas.

Gert
sumber
Terima kasih, jawaban Anda membuat saya melakukan pengambilan ganda terlambat: Saya menyadari saya benar-benar melewatkan bagian interaktif. Saya tidak berpikir itu bahkan mungkin dengan sed (tidak cukup saluran input / output).
Gilles 'SANGAT berhenti menjadi jahat'
1
Anda mungkin mempercepat ini dengan tidak membuka SEMUA file di buffer saat ini dengan menggunakan grepalih-alih findsehingga Anda hanya membuka file yang telah diketahui cocok. vim $(grep 'from' . -Rl)
Caleb
Terima kasih. C (astrerik di sekitar c) diperlukan? atau itu masalah pemformatan?
balki
@Balki itu masalah "pemformatan". Memperbaikinya
Gert
5

Anda dapat melakukan sesuatu yang kasar dengan skrip Perl kecil yang diperintahkan untuk melakukan penggantian baris demi baris ( -l -pe) pada file yang diteruskan sebagai argumen ( -i):

perl -i -l -pe '
    if (/from/) {                            # is the source text present on this line?
        printf STDERR ("%s: %s [y/N]? ", $ARGV, $_);  # display a prompt
        $r=<STDIN>;                                   # read user response
        if ($r =~ /^[Yy]/) {                          # if user entered Y:
            s/from/to/g;                              # replace all occurences on this line
    }' /path/to/files

Peningkatan yang mungkin dilakukan adalah mewarnai bagian prompt dan mendukung hal-hal seperti "ganti semua kejadian di file saat ini". Meminta secara terpisah untuk setiap kemunculan pada sebuah garis akan lebih sulit.

Bagian kedua, mencocokkan file. jika tidak terlalu banyak file yang terlibat dan Anda menjalankan zsh, Anda dapat mencocokkan semua file di direktori saat ini dan subdirektori secara rekursif:

perl -i -l -pe '…' **/*(.)

Jika shell Anda adalah bash ≥4, Anda dapat menjalankan perl … **/*, tetapi itu akan menghasilkan pesan kesalahan palsu karena sed akan mencoba (dan gagal) untuk berjalan pada direktori. Jika Anda hanya ingin melakukan penggantian dalam satu set file seperti file C, Anda dapat membatasi kecocokan (yang bekerja pada bash ≥4 atau zsh):

perl -i -l -pe '…' **/*.[hc]

Jika Anda perlu kontrol yang lebih baik atas file mana yang Anda ganti, atau shell Anda tidak memiliki konstruksi pencocokan direktori rekursif **, atau jika Anda memiliki terlalu banyak file dan mendapatkan kesalahan "baris perintah terlalu lama", gunakan find. Misalnya, untuk melakukan penggantian di semua file bernama *.hatau *.cdi direktori saat ini dan subdirektori (pada sistem yang lebih lama, Anda mungkin perlu menggunakan \;alih-alih +di akhir baris ( +formulir lebih cepat tetapi tidak tersedia di mana-mana).

find . -type f -name '*.[hc]' -exec perl -i -l -pe '…' {} +

Yang sedang berkata, saya akan tetap menjadi editor interaktif jika Anda membutuhkan interaksi. Gert telah menunjukkan cara untuk melakukan ini di Vim , meskipun itu mengharuskan membuka semua file yang ingin Anda cari, yang mungkin menjadi masalah jika ada banyak.

Di Emacs, inilah cara Anda dapat melakukan ini:

  1. Kumpulkan nama file dengan M-x find-name-dired(tentukan direktori tingkat atas) atau M-x find-dired(tentukan findbaris perintah arbitrer ).
  2. Di buffer dired yang dihasilkan , tekan tuntuk menandai semua file, lalu Q( dired-do-query-replace-regexp) untuk melakukan penggantian dengan meminta pada file yang ditandai.
Gilles 'SANGAT berhenti menjadi jahat'
sumber
1

sdiff(lihat http://www.gnu.org/software/diffutils/manual/diffutils.html#Invoking-sdiff ) mungkin berguna di sini. Dengan itu Anda dapat melakukan penambalan interaktif. Jadi melakukannya dengan file sementara yang Anda buat dengan melakukan operasi penggantian menggunakan sedmungkin merupakan solusi yang mungkin:

# use file descriptor 3 to still allow use of stdin
while IFS= read -r -d '' file <&3; do

  # write the result of the replacement into a temporary file
  sed -r 's/something/something_else/g' -- "$file" > replacer_tmp

  if cmp -s -- "$file" replacer_tmp; then
    continue; # nothing was replaced
  fi

  echo "There is something to replace in '$file'! Starting interactive diff."
  echo

  sdiff -o "$file" -s -d -- "$file" replacer_tmp

  echo

done 3< <(find . -type f -print0)

(File loop menggunakan subtitusi proses non-POSIX dan read -dseperti yang didukung misalnya oleh bash.)

phk
sumber