Как сделать замену текста в большой иерархии папок?

11

Я хочу найти и заменить текст в большом наборе файлов, исключая некоторые экземпляры. Для каждой строки я хочу, чтобы меня спрашивали, нужно ли мне заменить эту строку или нет. Нечто похожее на vim :%s/from/to/gccзапросом подтверждения), но через набор папок. Есть ли хороший инструмент командной строки или сценарий, который можно использовать?

Балки
источник
О важности правильного форматирования: я сначала прочитал бы вашу команду как s/from/to/gс ошибкой форматирования после нее, а не s/from/to/gcс акцентом на то, cкак вы пытались написать (вы не можете сделать это с Markdown, вы можете сделать это с <code>и <strong>HTML-теги).
Жиль "ТАК - перестань быть злым"

Ответы:

19

Почему бы не использовать Vim?

Откройте все файлы в VIM

vim $(find . -type f)

Или открывайте только соответствующие файлы (как предложено Caleb)

vim $(grep 'from' . -Rl)

И затем запустите замену во всех буферах

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

Вы также можете сделать это sed, но мои знания Sed ограничены.

Герт
источник
Спасибо, ваш ответ заставил меня сделать двойной дубль: я понял, что полностью пропустил интерактивный бит. Я не думаю, что это даже возможно с помощью sed (недостаточно каналов ввода / вывода).
Жиль "ТАК - перестань быть злым"
1
Вы можете ускорить это, не открывая ВСЕ файлы в текущем буфере, используя grepвместо того, findчтобы открывать только файлы с известными совпадениями. vim $(grep 'from' . -Rl)
Калеб
Спасибо. C (astreriks вокруг c) нужен? или это проблема форматирования?
Балки
@balki это проблема "форматирования". Исправлено
Герт
5

Вы можете сделать что-то грубое с небольшим скриптом Perl, который инструктируется выполнять построчную замену -l -peфайлов, передаваемых в качестве аргументов ( -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

Возможные улучшения будут состоять в том, чтобы раскрасить части приглашения и поддерживать такие вещи, как «заменить все вхождения в текущем файле». Отдельно подсказывать для каждого вхождения на линии будет сложнее.

Вторая часть, сопоставление файлов. если задействовано не слишком много файлов и вы используете zsh, вы можете рекурсивно сопоставить все файлы в текущем каталоге и его подкаталогах:

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

Если ваша оболочка bash ≥4, вы можете запустить perl … **/*, но это приведет к ложным сообщениям об ошибках, потому что sed будет пытаться (и не сможет) запускаться в каталогах. Если вы хотите выполнить замену только в наборе файлов, таких как файлы C, вы можете ограничить совпадения (это работает в bash ≥4 или zsh):

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

Если вам нужен более точный контроль над тем, какие файлы вы заменяете, или у вашей оболочки нет рекурсивной конструкции соответствия каталогов **, или если у вас слишком много файлов и выдается ошибка «слишком длинная командная строка», используйте find. Например, чтобы выполнить замену во всех именованных файлах *.hили *.cв текущем каталоге и его подкаталогах (в старых системах вам может потребоваться использовать \;вместо +конца строки ( +форма быстрее, но не везде доступна).

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

При этом я бы предпочел интерактивный редактор, если вам нужно взаимодействие. Герт показал путь к этому в Vim , хотя для этого нужно открыть все файлы, которые вы хотите найти, что может быть проблемой, если их много.

В Emacs вот как вы можете это сделать:

  1. Соберите имена файлов с помощью M-x find-name-dired(укажите каталог верхнего уровня) или M-x find-dired(укажите произвольную findкомандную строку).
  2. В полученном буфере с задержкой нажмите, tчтобы пометить все файлы, затем Q( dired-do-query-replace-regexp), чтобы выполнить замену с предложением пометить файлы.
Жиль "ТАК - перестань быть злым"
источник
1

sdiff(см. http://www.gnu.org/software/diffutils/manual/diffutils.html#Invoking-sdiff ) может пригодиться здесь. С его помощью вы можете делать интерактивные исправления. Поэтому возможно сделать это с временным файлом, который вы создали, используя операции замены sed:

# 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)

(Цикл файла с использованием подстановки процесса без POSIX и read -dкак поддерживается, например bash.)

PHK
источник