Найти и заменить строку и распечатать каталог файлов при изменении - PullRequest
1 голос
/ 13 апреля 2019

Я использую find и sed для замены строки в нескольких файлах.Вот мой сценарий:

find ./ -type f -name "*.html" -maxdepth 1 -exec sed -i '' "s/${REPLACE_STRING}/${STRING}/g" {} \; -print

-print всегда печатает файл независимо от того, было ли что-то изменено или нет.Что бы я хотел увидеть, какие файлы изменены.В идеале я хотел бы, чтобы вывод был примерно таким (так как файлы меняются):

/path/to/file was changed
  - REPLACE STRING line 9 was changed
  - REPLACE STRING line 12 was changed
  - REPLACE STRING line 26 was changed
/path/to/file2 was changed
  - REPLACE STRING line 1 was changed
  - REPLACE STRING line 6 was changed
  - REPLACE STRING line 36 was changed

Есть ли способ сделать что-то подобное?

Ответы [ 5 ]

0 голосов
/ 14 апреля 2019

легко установить бесплатно Perl, определите свои собственные строки в оболочке bash и протестируйте здесь:

STRING=
REPLACE=

perl -ne 'foreach(`find . -maxdepth 1 -type f -iname "*.html"`){ open IH,$_ or die "Error $!"; print "Processing: $_";while (<IH>) {$s=$_;$t=s/$REPLACE/$STRING/; print "$s --> $_" if $t };print "Nothing replaced" if !$t}'

, чтобы действительно отредактировать его, добавьте опцию -i, чтобы оно было perl -i -ne ....

0 голосов
/ 13 апреля 2019

Хорошо, всегда обращайтесь к сценарию awk Эда по эффективности, но продолжая сценарий sed + helper, используя предварительный вызов grep, чтобы определить, содержит ли ваш файл слово для замены, вы можете использоватькороткий вспомогательный сценарий, принимающий ${REPLACE_STRING}, ${STRING} и filename в качестве первых трех позиционных параметров следующим образом:

вспомогательный сценарий с именем helper.sh

#!/bin/sh

test -z "$1" && exit
test -z "$2" && exit
test -z "$3" && exit

findw="$1"
replw="$2"
fname="$3"

grep -q "$findw" "$fname" || exit

echo "$(readlink -f $fname) was changed"
grep -n "$findw" "$fname" | {
while read line; do
    printf -- "  - REPLACE STRING line %d was changed\n" "${line%:*}"
done }

sed -i "s/$findw/$replw/g" "$fname"

Тогда ваш вызов find может быть, например:

find . -type f -name "f*" -exec ./helper.sh "dog" "cat" '{}' \;

Пример использования / Вывод

Начиная с пары файлов с именем fсодержащий:

$ cat f
my
dog
dog
has
fleas

В файловой структуре, содержащей скрипт в текущем каталоге с подкаталогом d1 и несколькими копиями f, например

$ tree .
.
├── d1
│   └── f
├── f
└── helper.sh

Запуск результатов скриптав следующем:

$ find . -type f -name "f*" -exec ./helper.sh "dog" "cat" '{}' \;
/tmp/tmp-david/f was changed
  - REPLACE STRING line 2 was changed
  - REPLACE STRING line 3 was changed
/tmp/tmp-david/d1/f was changed
  - REPLACE STRING line 2 was changed
  - REPLACE STRING line 3 was changed

и содержание f соответствующим образом изменяются

$ cat f
my
cat
cat
has
fleas

Если ни в одном из файлов, расположенных в find, не найден поисковый термин,время модификации этих файлов остается неизменным.

Теперь, учитывая все это, если у вас есть gawk, следуйте советам Эда, но - вы можете сделать это с sed и помощником :)

0 голосов
/ 13 апреля 2019

Вы можете связать -exec действий и воспользоваться статусом выхода. Например:

find . \
    -maxdepth 1 \
    -type f \
    -name '*.html' \
    -exec grep -Hn "$REPLACE_STRING" {} \; \
    -exec sed -i '' "s/${REPLACE_STRING}/${STRING}/g" {} \;

Для каждого соответствующего файла выводятся путь, номер строки и строка:

./file1.html:9:contents of line 9
./file1.html:12:contents of line 12
./file1.html:26:contents of line 26
./file2.html:1:contents of line 1
./file2.html:6:contents of line 6
./file2.html:36:contents of line 36

Для файлов без совпадений больше ничего не происходит; для файлов с совпадением будет вызвана команда sed.

Если вы хотите, чтобы результат был ближе к тому, что есть в вашем вопросе, вы можете добавить несколько действий:

find . \
    -maxdepth 1 \
    -type f \
    -name '*.html' \
    -exec grep -q "$REPLACE_STRING" {} \; \
    -printf '%p was changed\n' \
    -exec grep -n "$REPLACE_STRING" {} \; \
    -exec sed -i '' "s/${REPLACE_STRING}/${STRING}/g" {} \; \
    | sed -E "s/^([[:digit:]]+):.*/  - $REPLACE_STRING line \1 was changed/"

Теперь это сначала проверяет, содержит ли файл строку без вывода сообщений grep -q, затем печатает имя файла (-printf), затем все совпадающие строки с номерами строк (grep -n), а затем выполняет замену с помощью sed и, наконец, слегка изменяет вывод с помощью sed.

Поскольку вы используете sed -i '', я предполагаю, что вы работаете в macOS; Я не уверен, что акция find там поддерживает опцию printf.

К настоящему времени мы довольно близки к выполнению сценария комплексного вывода для каждого соответствующего файла, поэтому мы могли бы также сделать это напрямую:

shopt -s nullglob
for f in ./*.html; do
    if grep -q "$REPLACE_STRING" "$f"; then
        printf '%s\n' "$f was changed"
        grep -n "$REPLACE_STRING" "$f" \
            | sed -E "s/^([[:digit:]]+):.*/  - $REPLACE_STRING line \1 was changed/"
        sed -i '' "s/${REPLACE_STRING}/${STRING}/g" "$f"
    fi
done
0 голосов
/ 13 апреля 2019

Замените команду find + sed:

find ./ -type f -name "*.html" -maxdepth 1 -exec sed -i '' "s/${REPLACE_STRING}/${STRING}/g" {} \; -print

этой командой GNU awk (требуется gawk для редактирования на месте):

gawk -i inplace -v old="$REPLACE_STRING" -v new="$STRING" '
    FNR==1 { hdr=FILENAME " was changed\n" }
    gsub(old,new) { printf "%s  - %s line %d was changed\n", hdr, old, FNR | "cat>&2"; hdr="" }
1' *.html

Вы также можете сделать ее намного более надежной с помощьюawk, чем с sed, если необходимо, поскольку awk может поддерживать буквенные строки, а sed не может

0 голосов
/ 13 апреля 2019

Классная идея. Я думаю, что -print является отступной по той причине, которую вы упомянули, поэтому это нужно сделать в exec. Я думаю, что sed также является недостатком из-за проблемы печати до STDOUT, а также изменения файла. Поэтому естественным продолжением является обертывание вокруг него некоторого Perl.

Что, если это было ваше exec утверждение:

perl -p -i -e '$i=1 if not defined($i); print STDOUT "$ARGV, line $i: $_" if s/REPLACE_STRING/STRING/; $i++' {} \;
  • -p упаковывает операторы Perl в стандартный цикл while(<>), поэтому файл обрабатывается построчно, как sed.
  • -i выполняет замену на месте, точно так же, как sed.
  • -e означает выполнение следующих операторов Perl.
  • if not defined - хитрый способ инициализации переменной числа строк, даже если она выполняется для каждой строки.
  • STDOUT указывает print выводить на консоль вместо файла.
  • $ARGV - текущее имя файла при чтении с <>.
  • $_ - обрабатываемая строка.
  • if означает, что print выполняется, только если найдено совпадение.

Для входного файла text.txt, содержащего:

line 1
token 2
line 3
token 4
line 5

Утверждение perl -p -i -e '$i=1 if not defined($i); print STDOUT "$ARGV, line $i: $_" if s/token/sub/; $i++' text.txt дает мне:

text.txt, line 2: sub 2
text.txt, line 4: sub 4

Выход text.txt, содержащий:

line 1
sub 2
line 3
sub 4
line 5

Таким образом, вы не получите вводную строку "файл был изменен", но для однострочника я думаю, что это довольно хороший компромисс.

При работе с парой файлов это выглядит так:

find ./ -type f -name "*.txt" -maxdepth 1 -exec perl -p -i -e '$i=1 if not defined($i); print STDOUT "$ARGV, line $i: $_" if s/token/sub/; $i++' {} \;
.//text1.txt, line 2: sub 2
.//text1.txt, line 4: sub 4
.//text2.txt, line 1: sub 1
.//text2.txt, line 3: sub 3
.//text2.txt, line 5: sub 5
...