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

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

Я ищу способ заменить строки-заполнители в файле шаблона на конкретные значения с помощью обычных инструментов Unix (bash, sed, awk, возможно perl). Важно, чтобы замена была выполнена за один проход, то есть то, что уже просканировано/заменено, не должно рассматриваться для другой замены. Например, эти две попытки не увенчались успехом:

echo "AB" | awk '{gsub("A","B");gsub("B","A");print}'
>> AA

echo "AB" | sed 's/A/B/g;s/B/A/g'
>> AA

Правильный результат в этом случае, конечно же, BA.

В общем случае решение должно быть эквивалентно сканированию ввода слева направо для нахождения самого длинного совпадения с одной из заданных строк замены, и для каждого совпадения, выполнению замены и продолжению с этой точки ввода (ни один из уже прочитанных вводов, ни выполненные замены не должны рассматриваться для совпадений). На самом деле, детали не важны, важно только то, что результаты замены никогда не рассматриваются для другой замены, полностью или частично.

ПРИМЕЧАНИЕЯ ищу только правильные общие решения. Пожалуйста, не предлагайте решения, которые не работают для определенных входных данных (входные файлы, пары поиска и замены), какими бы маловероятными они ни казались.

решение1

Хорошо, общее решение. Следующая функция bash требует 2kаргументов; каждая пара состоит из заполнителя и замены. Вам нужно правильно заключить строки в кавычки, чтобы передать их в функцию. Если число аргументов нечетное, будет добавлен неявный пустой аргумент, который фактически удалит вхождения последнего заполнителя.

Ни заполнители, ни замены не могут содержать символы NUL, но вы можете использовать стандартные экранированные символы C, \например, \0если вам нужен NULs (и, следовательно, вам необходимо написать , \\если вам нужен \).

Для этого требуются стандартные инструменты сборки, которые должны присутствовать в posix-подобной системе (lex и cc).

replaceholder() {
  local dir=$(mktemp -d)
  ( cd "$dir"
    { printf %s\\n "%option 8bit noyywrap nounput" "%%"
      printf '"%s" {fputs("%s", yyout);}\n' "${@//\"/\\\"}"
      printf %s\\n "%%" "int main(int argc, char** argv) { return yylex(); }"
    } | lex && cc lex.yy.c
  ) && "$dir"/a.out
  rm -fR "$dir"
}

Мы предполагаем, что \уже экранировано, если необходимо, в аргументах, но нам нужно экранировать двойные кавычки, если они есть. Это то, что делает второй аргумент второго printf. Поскольку действие lexпо умолчанию — ECHO, нам не нужно об этом беспокоиться.

Пример запуска (с указанием времени для скептиков; это всего лишь дешевый ноутбук):

$ time echo AB | replaceholder A B B A
BA

real    0m0.128s
user    0m0.106s
sys     0m0.042s
$ time printf %s\\n AB{0000..9999} | replaceholder A B B A > /dev/null

real    0m0.118s
user    0m0.117s
sys     0m0.043s

Для больших входных данных может быть полезно предоставить флаг оптимизации для cc, а для текущей совместимости с Posix лучше использовать c99. Еще более амбициозная реализация может попытаться кэшировать сгенерированные исполняемые файлы вместо того, чтобы генерировать их каждый раз, но их генерация не так уж и затратна.

Редактировать

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

treplaceholder () { 
  tcc -run <(
  {
    printf %s\\n "%option 8bit noyywrap nounput" "%%"
    printf '"%s" {fputs("%s", yyout);}\n' "${@//\"/\\\"}"
    printf %s\\n "%%" "int main(int argc, char** argv) { return yylex(); }"
  } | lex -t)
}

$ time printf %s\\n AB{0000..9999} | treplaceholder A B B A > /dev/null

real    0m0.039s
user    0m0.041s
sys     0m0.031s

решение2

printf 'STRING1STRING1\n\nSTRING2STRING1\nSTRING2\n' |
od -A n -t c -v -w1 |
sed 's/ \{1,3\}//;s/\\$/&&/;H;s/.*//;x
     /\nS\nT\nR\nI\nN\nG\n1/s//STRING2/
     /\nS\nT\nR\nI\nN\nG\n2/s//STRING1/
     /\\n/!{x;d};s/\n//g;s/./\\&/g' |
     xargs printf %b

###OUTPUT###

STRING2STRING2

STRING1STRING2
STRING1

Что-то вроде этого всегда будет заменять каждое вхождение ваших целевых строк только один раз, поскольку они встречаются в sed's в потоке по одному байту на строку. Это самый быстрый способ, который я могу себе представить, чтобы вы это сделали. С другой стороны, я не пишу на C. Но этоделаетнадежно обрабатывать нулевые разделители, если вы этого хотите. Смотретьэтот ответдля того, как это работает. Это не имеет проблем с любыми содержащимися специальными символами оболочки или подобными - но этоявляетсяСпецифическая локаль ASCII, или, другими словами, odне будет выводить многобайтовые символы на одной строке и будет выводить только один на. Если это проблема, вам нужно будет добавить в iconv.

решение3

Решение perl. Даже если некоторые утверждали, что это невозможно, я нашел одно, но в целом простое сопоставление и замена невозможны, и даже становится хуже из-за возврата NFA, результат может быть неожиданным.

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

A B
AA CC

AAAи результат ввода — BBBили CCB.

Вот код:

#!/usr/bin/perl

$v='if (0) {} ';
while (($a,$b)=split /\s+/, <DATA>) {
  $k.=$a.'|';
  $v.='elsif ($& eq \''.$a.'\') {print \''.$b.'\'} ';
}
$k.='.';
$v.='else {print $&;}';

eval "
while (<>) {
  \$_ =~ s/($k)/{$v}/geco;
}";  
print "\n";


__DATA__
A    B
B    A
abba baab
baab abbc
abbc aaba

Шашечник:

$ echo 'ABBabbaBBbaabAAabbc'|perl script
$ BAAbaabAAabbcBBaaba

Связанный контент