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


11

Я ищу способ заменить строки-заполнители в файле шаблона конкретными значениями с помощью общих инструментов 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.

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

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


Я полагаю, они длиннее, чем один персонаж? Для этого вы можете использовать tr AB BA.
Кевин

3
И, честно говоря, я не удивлюсь, если кто-то посчитает вашу записку немного грубой.
Петер

1
Как вы ожидаете «получать правильные решения» только тогда, когда вы не предоставили пример ввода или вывода?
jasonwryan

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

2
Это справедливый вопрос, но ответ заключается в том, что вам нужен анализатор конечного автомата , который дает ответ Ричи (я думаю, в истинном стиле хакера). Другими словами, вы недооцениваете сложность задачи, в частности «Я хочу обобщенно проанализировать (HT | X) ML с регулярными выражениями» -> Ответ НЕТ. Вы не можете (просто) использовать sed. Вы не можете (просто) использовать awk. AFAIK нет никакого существующего инструмента, который сделает это из коробки. Sans rici подвиг, вам нужно написать код.
Златовласка

Ответы:


10

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

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

Это требует стандартных инструментов сборки, которые должны присутствовать в 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. Еще более амбициозная реализация может попытаться кэшировать сгенерированные исполняемые файлы, а не генерировать их каждый раз, но генерировать их не совсем дорого.

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

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

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

Я не уверен, если это шутка или нет;)
Амброз Бижак

3
@ambrozbizjak: Это работает, это быстро для больших входов и приемлемо быстро для маленьких входов. Он может не использовать инструменты, о которых вы думали, но это стандартные инструменты. Почему это будет шутка?
Ричи

4
+1 Не шутка! : D
Златовласка

Это было бы портативным как POSIX fn() { tcc ; } <<CODE\n$(gen code)\nCODE\n. Могу ли я спросить - это потрясающий ответ, и я проголосовал за него, как только прочитал, - но я не понимаю, что происходит с массивом оболочки? Что "${@//\"/\\\"}"это делает?
mikeserv

@mikeserv: «Для каждого аргумента в качестве значения в кавычках (" $ @ ") замените все (//) вхождения кавычки (\") на (/) обратную косую черту (\\), за которой следует кавычка (\ ") ». См. Расширение параметров в руководстве по bash.
Ричи

1
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в потоке с одним битом на строку. Это самый быстрый способ, которым я могу представить, что ты это сделаешь. Опять же , я не пишу C. Но это действительно надежно обрабатывать пустые разделители , если вы хотите его. Посмотрите этот ответ, чтобы узнать, как это работает. Это не имеет проблем с какими-либо содержащимися специальными символами оболочки или аналогичными - но это зависит от локали ASCII, или, другими словами, odне будет выводить многобайтовые символы в одной строке и будет делать только один для каждого. Если это проблема, которую вы хотите добавить iconv.


+1 Почему вы говорите, что он заменяет только «самое раннее появление ваших целевых строк»? На выходе это выглядит так, как будто он заменяет их все. Я не прошу это увидеть, но можно ли это сделать таким образом, без жесткого кодирования значений?
Златовласка

@goldilocks - Да - но только, как только они происходят. Может быть, я должен перефразировать это. И да - вы могли бы просто добавить середину sedи сохранить до нуля или чего-то еще, чтобы sedнаписать сценарий этого; или поместите его в функцию оболочки и присвойте ему значения по одному биту на строку, например "/$1/"... "/$2/"- может быть, я тоже напишу эти функции ...
mikeserv

Это не работает в случае, когда заполнители PLACE1, PLACE2и PLA. PLAвсегда побеждает. ОП говорит: «эквивалентно сканированию входных данных слева направо на предмет наибольшего совпадения с одной из заданных замещающих строк» ​​(выделение добавлено)
Ричи

@rici - спасибо. Тогда мне придется делать нулевые разделители. Вернуться в мгновение ока.
mikeserv

@rici - я как раз собирался опубликовать другую версию, которая будет обрабатывать то, что вы описываете, но смотрю на это снова, и я не думаю, что должен. Он говорит, дольше всего для одной из заданных строк замены. Это делает это. Нет указаний на то, что одна строка является подмножеством другой, может быть только замененное значение. Я также не думаю, что перебор списка является правильным способом решения проблемы. Учитывая проблему, насколько я понимаю, это рабочее решение.
mikeserv

1

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

Checkerbunny:

$ echo 'ABBabbaBBbaabAAabbc'|perl script
$ BAAbaabAAabbcBBaaba
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.