Замена текста в нескольких файлах с текстом из списка в порядке

Скажем, у меня есть следующие файлы:

/etc/dir1/file.txt
/etc/dir2/file.txt
/etc/dir3/file.txt

... полностью до dir100 (100 каталогов), каждый каталог имеет file.txt.

И у меня есть следующий текстовый файл в /root/list.txt. В list.txt, У меня есть 100 строк, каждая строка с различной строкой текста.

В каждом file.txt, существует строка текста, word1.

Как я использовал бы sed (или что-то подобное) для замены слова word1 в каждом file.txt, с одной строкой от list.txt? Каждая строка в list.txt, только, чтобы использоваться однажды.

Так, например, замена word1 в /etc/dir1/file.txt с первой строкой в /root/list.txt, и замена word1 /etc/dir2/file.txt со второй строкой в /root/list.txt и так далее, полностью до 100.

Я значительно ценю любую справку и помощь здесь как sed не моя сильная сторона.

2
задан 4 September 2017 в 18:58

2 ответа

Можно сделать это с sed в цикле, если строки list.txt хорошего поведения.

Как я использовал бы sed (или что-то подобное) для замены слова word1 в каждом file.txt, с одной строкой от list.txt? Каждая строка в list.txt, только, чтобы использоваться однажды.

Ubuntu имеет GNU sed, который помогает заменить только первое вхождение шаблона в файле, затем остановиться. Для использования отдельных замещающих строк для каждого входного файла можно использовать цикл. Код ниже просто достаточно сложен, что я предлагаю делать его сценарием и запустить скрипт. Существует три главных протеста:

  1. Шаблон word1- то, которое я предполагаю, что можно измениться на что-то еще - не должно содержать /, если Вы не используете другой разделитель в sed команда. И при этом это не может содержать символы sed обработки особенно, такие как метасимволы регулярного выражения (\, *, ., и т.д), если это не то, что Вы предназначаете.
  2. Строки в list.txt не должен содержать / или наиболее специальные символы также.
  3. Ваш dir1, dir2..., находятся в /etc и Ваш list.txt находится в /root, но я записал свой сценарий, чтобы предположить, что те каталоги - и тот файл - находятся в текущем каталоге вместо этого. Я сделал это, потому что файлы, хранившие в тех местоположениях, часто важны, и я предполагаю, что Вы захотите протестировать этот сценарий - и возможно сделать Ваши собственные модификации - перед использованием его для реального. Можно изменить сценарий для использования местоположений, которые Вы дали, или любые другие местоположения, в которых Вы нуждаетесь.

У меня есть полужирный № 2 becuase, это - то, которое я ожидаю, мог бы вызвать Вас проблема, в зависимости от какой list.txt мог бы содержать. Теперь, когда Вас предупредили, вот сценарий:

#!/bin/bash

mapfile -t <list.txt

for ((i=1; i<=${#MAPFILE[@]}; ++i))
do sed -i.bak "0,/word1/ s//${MAPFILE[i-1]}/" "dir$i/file.txt"
done

Это - все, что требуется. В случае, если Вам интересно, вот то, как это работает:

  • mapfile Bash, встроенный, который читает строки в массив. Я использую его для чтения из list.txt. Я не указывал имя массива так имя по умолчанию MAPFILE используется.
  • Bash предлагает альтернативу (C-стиль) for цикл, который полезен, когда каждый хочет циклично выполниться от или до значения, полученного расширением параметра, начиная с расширения фигурной скобки в Bash, не развернет вещи как {1..$var}. Я использую его для цикличного выполнения от 1 до длины MAPFILE массив.
  • sed -i заменяет исходный файл. Вы теряете старую версию, если Вы не обеспечиваете резервный суффикс. Можно удалить .bak от команды, если Вы не хотите сохранять старую версию, но я рекомендую рассмотреть хранение ее. По крайней мере, тест с ним прежде, чем удалить его.
  • С GNU sed, 0,/word1/ s//REPLACEMENT/ работает только в диапазоне с начала файла к первому соответствию с word1 (0,/word1/), заменяя текст, соответствующий тому же самому шаблону REPLACEMENT (s//*REPLACEMENT*/). Это должно сказать, что заменяет только первое соответствие во входе. Остальная часть входа неизменна независимо от того, соответствовало ли это шаблону.
  • Массивы Bash индексируются, начиная с 0, но Ваши файлы называют, начиная с 1, который является, где я решил переменную цикла $i должен запуститься. К счастью, это легко иметь дело с тем, потому что массивы Bash принимают арифметические выражения как индексы. ${MAPFILE[i-1]} расширяется до элемента i - 1 (то есть, ith элемента) массива строк от list.txt.

Для замены использующего произвольного текста, прочитанного из файла, рассмотрите альтернативы sed.

Если Вы терпеть не можете протест № 2 - то есть, строки в list.txt могло быть примерно что-либо - затем я не знаю о хорошем способе сделать это с sed. Но существует много альтернатив. Bash может на самом деле сделать это само без любых внешних команд, и я покажу почти решение чистого Bash. (Возможно, больше ответов будет отправлено для показа использования методов awk или другие утилиты.)

В sed, шаблон является регулярным экспрессом. Но этот метод рассматривает шаблон как шарик. См. также эту запись FOLDOC и man 7 glob. Таким образом специальные символы включая *, ?, [, и ]- и некоторые другие как \- имейте особые значения в word1 (просто не обычно те же значения как в sed). Если word1 буквально word1 или любой другой текст без globbing символов, без проблем. Иначе необходимо изменить его соответственно. Этот метод удаляет протест № 2, но не протест № 1 - ни № 3, но можно иметь дело с этим сами легко.

#!/bin/bash

pattern='word1' # text to search for
suffix='.bak'   # suffix to append for backup files

mapfile -t <list.txt

for ((i=1; i<=${#MAPFILE[@]}; ++i)); do
    name="dir$i/file.txt"
    mv "$name" "$name$suffix" || exit  # quit with an error if we can't rename

    {
        while read -r; do  # output up to and including the replacement
            case "$REPLY" in
            *"$pattern"*)
                printf '%s\n' "${REPLY/$pattern/${MAPFILE[i-1]}}"
                break ;;
            *)
                printf '%s\n' "$REPLY" ;;
            esac
        done

        while read -r; do  # output the rest
            printf '%s\n' "$REPLY"
        done

    } <"$name$suffix" >"$name"
done

В случае, если Вы (все еще) заинтересованы, вот то, как это работает:

  • Как прежде, я читал list.txt в массив. Я мог также считать каждого file.txt в массив, но кто знает эти файлы могли быть огромными, таким образом, я читаю их одна строка за один раз с read -r вместо этого.
  • Я перемещаю каждый файл в сторону путем переименования его с a .bak суффикс с mv. mv единственная внешняя утилита этот сценарий использование. Я не потрудился передавать -- прежде чем путь, потому что в этом случае нет никакого пути пути, может запуститься с -. Если операция пересылки перестала работать, mv произведет ошибку и || exit завершает сценарий, предотвращая случайную потерю данных. Единственные данные, которые должно быть возможно потерять путем запущения этого скрипта, являются данными в существовании ранее .bak файлы.
  • Два цикла, которые читают вход и вывод записи, группируются с { }. У целой группы есть оба своих ввода и вывода, перенаправленные для использования .bak файл, как введено и файл, названный тем же как оригинал, как произведено (<"$name$suffix" >"$name").
  • Каждый цикл может считать много строк и единственный путь read -r самостоятельно изменяет их, должен удалить символы новой строки в концах (который printf возвратится с \n позже). В Bash, read -r без имени переменной читает строку в $REPLY и не разделяет ведущий и запаздывающий пробел; это эквивалентно IFS= read -r REPLY.
  • Первые чтения цикла до строки появляются состоящий из любого или никаких символов (*), сопровождаемый word1 ("$pattern"), сопровождаемый снова любым или никакими символами (*). Когда это находит такую строку, это печатает его, но заменяет часть, которая соответствует "$pattern" с ith строкой от list.txt (${MAPFILE[i-1]}) и затем повреждает цикл. Все строки перед той просто печатаются дословно.
  • Второй цикл печатает все остающиеся строки дословно.

При помощи двух группировавшихся циклов я достиг той же основной логики как sed путь детализировал выше - текст до, включая, но не вне первого соответствия обрабатывается сначала, таким образом, соответствием заменяют, затем последующий текст не ищется вообще, просто копируется. Однако, в отличие от этого, в этом sed метод, специальные символы, в какой ${MAPFILE[i-1]} расширяется до, не рассматриваются как часть команды.

Например, заметьте, что замещающая строка, которая пыталась доставить неприятности путем закрытия внутреннего расширения параметра и введения дополнительных замен, не успешно выполнится:

$ s=foobarbaz t=bar u='}$s$s$s'; echo "${s/$t/${u}}"
foo}$s$s$sbaz
3
ответ дан 2 December 2019 в 02:19

Давайте создадим тестовую среду, помещенную в пользователя $HOME каталог.

  • Сначала выполните следующую строку как единственную команду:

    path="${HOME}/etc/dir"; for i in {1..100}; do mkdir -p "$path$i" ; echo -e "$path$i/file.txt:\nline1 some text here\nline2 word1 some text here word1\nline3 word1 some text here" > "$path$i/file.txt"; done
    

    Это создаст сотню каталогов - ~/etc/dir{1..100}. В каждом каталоге будет создан также файл, названный file.txt, это содержит строку word1 несколько раз:

    $ cat ~/etc/dir{1..100}/file.txt
    /home/<user>/etc/dir1/file.txt:
    line1 some text here
    line2 word1 some text here word1
    line3 word1 some text here
    /home/<user>/etc/dir2 file.txt: 
    ...
    
  • Затем выполните эту строку:

    path="${HOME}/root" && mkdir "$path"; for i in {1..100}; do echo '*{string line ['"$i"']}*' >> "$path/list.txt"; done
    

    Это создаст каталог, названный ~/root. В каталоге будет создан также файл, названный list.txt, это содержит сотню строк:

    $ cat ~/root/list.txt
    *{string line [1]}*
    *{string line [2]}*
    ...
    

Давайте решим задачу. Согласно обстоятельствам, создайте в вышеупомянутый шаг, потому что строка word1 несколько раз происходит, у нас есть несколько случаев. Решения в качестве примера:

  • Заменять только первое вхождение word1 в каждом file.txt, выполните эту строку:

    i=""; while read line; do i=$((i+1)); sed "0,/word1/ s|\word1|${line}|1" "$HOME/etc/dir$i/file.txt"; done < "$HOME/root/list.txt"
    

    Вывод должен быть:

    /home/<user>/etc/dir1/file.txt:
    line1 some text here
    line2 *{string line [1]}* some text here word1
    line3 word1 some text here
    /home/<user>/etc/dir2 file.txt:
    ...
    
  • Заменять только первое вхождение word1 на каждой строке в каждом file.txt, выполните эту строку:

    i=""; while read line; do i=$((i+1)); sed "s|\word1|${line}|1" "$HOME/etc/dir$i/file.txt"; done < "$HOME/root/list.txt"
    

    Вывод должен быть:

    /home/<user>/etc/dir1/file.txt:
    line1 some text here
    line2 *{string line [1]}* some text here word1
    line3 *{string line [1]}* some text here
    /home/<user>/etc/dir2 file.txt:
    ...
    
  • Заменять все случаи word1 в каждом file.txt, выполните эту строку:

    i=""; while read line; do i=$((i+1)); sed "s|\word1|${line}|g" "$HOME/etc/dir$i/file.txt"; done < "$HOME/root/list.txt"
    

    Вывод должен быть:

    /home/<user>/etc/dir1/file.txt:
    line1 some text here
    line2 *{string line [1]}* some text here *{string line [1]}*
    line3 *{string line [1]}* some text here
    /home/<user>/etc/dir2 file.txt:
    ...
    

Примечания. В вышеупомянутые примеры:

  • Изменение sed с sed -i для фактической замены строк или использования sed -i.bak сделать замены и оставить также файл резервной копии.

  • Удалить $HOME согласно обстоятельствам, описанным в вопрос.

1
ответ дан 2 December 2019 в 02:19

Другие вопросы по тегам:

Похожие вопросы: