Мне нужно запустить функцию поиска/замены в большом текстовом файле ascii. Небольшой отрывок из входного файла:

gene_id "MSTRG.1";
gene_id "MSTRG.1";
gene_id "MSTRG.2";
gene_id "MSTRG.3";

Строка MSTRG будет заменена другим идентификатором, присутствующим в файле шаблона:

MSTRG.1 AT1G01030
MSTRG.2 AT1G01010
MSTRG.3 AT1G01035

Простой цикл while перебирает каждую строку шаблона и делает замены:

while read bef aft
do
  echo "Searching for $bef"
  echo "Replacing with $aft"
  sed "s/$bef/$aft/g" input > output
done < template

Происходит то, что MSTRG.2 и последующие записи заменяются правильно, но MSTRG.1 остается неизменным. Вывод выглядит следующим образом:

gene_id "MSTRG.1";
gene_id "MSTRG.1";
gene_id "AT1G01010";
gene_id "AT1G01035"; 

Обновить

Вот что я сделал в конце.

while read bef aft
do
  sed -i "s/$bef/$aft/g" input
done < template
2
cryptic0 15 Окт 2019 в 22:49
Я немного удивлен, что MSTRG.2 заменяется в финальном файле output. Вы стираете файл на каждой итерации, поэтому я ожидаю, что будет видна только последняя замена. Просто выполните перенаправление на output после цикла, а не на каждой итерации (точно так же, как вы уже перенаправляете ввод цикла из template). Я не публикую это как ответ, так как не понимаю, почему вы говорите (или как это возможно), что MSTRG.2 заменяется.
 – 
Kusalananda
15 Окт 2019 в 19:04
Не уверен, что понимаю. Как перенаправить вывод после завершения цикла?
 – 
cryptic0
15 Окт 2019 в 19:11
while ...; do ...; done <template >output. И перенаправьте два вызова echo на стандартную ошибку, чтобы они не попали в вывод: echo ... >&2.
 – 
Kusalananda
15 Окт 2019 в 19:26
 – 
Philippos
15 Окт 2019 в 22:44
Вам вообще не нужен цикл read, но сделайте это за один проход, адаптировав схему, приведенную в связанном ответе.
 – 
Philippos
15 Окт 2019 в 22:46

3 ответа

Вместо того, чтобы перезаписывать выходной файл в каждой итерации цикла, вы можете копировать входной файл как выходной файл и работать с этим выходным файлом.

С параметром -i sed изменения записываются на месте в тот же файл, поэтому предыдущие замены не теряются:

cp input output
while read bef aft
do
  echo "Searching for $bef"
  echo "Replacing with $aft"
  sed -i "s/$bef/$aft/g" output
done < template
1
Freddy 15 Окт 2019 в 22:52
Именно то, что я только что сделал (и опубликовал обновление на мой вопрос). Спасибо.
 – 
cryptic0
15 Окт 2019 в 22:53
Да, ты был слишком быстр! Я только что понял ваше обновление после моего поста.
 – 
Freddy
15 Окт 2019 в 22:54

Ваша проблема в том, что вы затираете выходной файл на каждой итерации цикла, оставляя только самые последние изменения в output и ни одно из более ранних.

Вместо этого вы можете легко преобразовать файл template в серию команд sed:

$ awk '{ printf("s/%s/%s/g\n", $1, $2) }' template
s/MSTRG.1/AT1G01030/g
s/MSTRG.2/AT1G01010/g
s/MSTRG.3/AT1G01035/g

... а затем примените их к своему файлу:

$ awk '{ printf("s/%s/%s/g\n", $1, $2) }' template | sed -f - input
gene_id "AT1G01030";
gene_id "AT1G01030";
gene_id "AT1G01010";
gene_id "AT1G01035";

Некоторые реализации sed не распознают - как стандартный ввод. Чтобы использовать этот подход с таким sed, замените -f - на -f /dev/stdin.

Или вы можете просто сделать все это в awk:

$ awk 'FNR == NR { pat[$1] = $2; next } { for (p in pat) gsub(p, pat[p]); print }' template input
gene_id "AT1G01030";
gene_id "AT1G01030";
gene_id "AT1G01010";
gene_id "AT1G01035";

Обратите внимание, что все приведенные выше варианты используют то, что находится в первом столбце template, как регулярное выражение, а это означает, что . (точка) будет соответствовать любому< /em> символ.

2
Kusalananda 16 Окт 2019 в 12:16
#!/usr/bin/perl -i

use strict;

# The %re hash holds the regexp searches and replacement strings.
my %re = ();

my $tfile = shift;
open(TEMPLATE, "<", $tfile) || die "couldn't open $tfile for read: $!\n";
while(<TEMPLATE>) {
   chomp;
   my ($search,$replace) = split;
   $re{qr/$search/} = $replace;
};
close(TEMPLATE);

while (<>) {
  foreach my $s (keys %re) {
    s/$s/$re{$s}/g;
  };
  print;
}

Это читает файл template и создает ассоциативный массив (также известный как «хэш»), называемый %re поиска и замены регулярных выражений.

Затем он перебирает каждое оставшееся имя файла в командной строке (например, input) и выполняет все эти операции поиска и замены в каждой строке ввода. Он использует qr// для предварительной компиляции регулярных выражений - это всего лишь тривиальная оптимизация, если в template не так много строк, но может привести к очень значительному ускорению, если строк много.

-i в строке #!/usr/bin/perl -i заставляет perl вносить изменения во входные файлы на месте, а не просто выводить изменения на стандартный вывод. Измените это, например, на -i.bak, если вы хотите, чтобы сохранялась резервная копия файлов до того, как они были изменены.

Сохраните как, например, cryptic0.pl, сделайте его исполняемым с помощью chmod +x cryptic0.pl и запустите его следующим образом:

$ ./cryptic0.pl template input

Скрипт не будет производить вывод на терминал. Вместо этого он будет редактировать входной файл(ы).

Например, ваш файл input будет изменен на:

$ cat input
gene_id "AT1G01030";
gene_id "AT1G01030";
gene_id "AT1G01010";
gene_id "AT1G01035";

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

s/$s/$re{$s}/g;

К этому:

s/$s/$re{$s}/ && last;

Это приводит к тому, что скрипт пропускает цикл foreach к оператору print, а затем переходит к следующей строке ввода, как только он успешно выполнил поиск и замену.


Кстати, см. Почему использование цикла оболочки для обработки текста считается плохой практикой? почему не рекомендуется выполнять обработку текста с помощью циклов sh. Используйте awk или perl или sed или что-то еще вместо sh или bash.

1
cas 16 Окт 2019 в 11:58