Как использовать регулярные выражения с AWK для замены строк?


13

Предположим, есть текст из файла:

(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 23" "#2")
("Exercises 31" "#30")
("Notes and References 42" "#34"))
)

Я хочу добавить 11 к каждому числу, за которым следует a "в каждой строке, если оно есть, т.е.

(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 34" "#13")
("Exercises 42" "#41")
("Notes and References 53" "#45"))
)

Вот мое решение с использованием GNU AWK и regex:

awk -F'#' 'NF>1{gsub(/"(\d+)\""/, "\1+11\"")}'

т.е. я хочу заменить (\d+)\"на \1+10\", где \1группа, представляющая (\d+). Но это не работает. Как я могу заставить это работать?

Если gawk не лучшее решение, что еще можно использовать?


Извините за дублирование. Но я сначала спросил о stackoverflow и не получил удовлетворительного ответа, поэтому пометил для миграции. Но это не происходило какое-то время, поэтому я не ожидал, что это произойдет, а затем спросил об Unix.SE.
Тим

Ответы:


12

Попробуй это (ткнуть нужно).

awk '{a=gensub(/.*#([0-9]+)(\").*/,"\\1","g",$0);if(a~/[0-9]+/) {gsub(/[0-9]+\"/,a+11"\"",$0);}print $0}' YourFile

Протестируйте на своем примере:

kent$  echo '(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 2" "#2")
("Exercises 30" "#30")
("Notes and References 34" "#34"))
)
'|awk '{a=gensub(/.*#([0-9]+)(\").*/,"\\1","g",$0);if(a~/[0-9]+/) {gsub(/[0-9]+\"/,a+11"\"",$0);}print $0}'   
(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 13" "#13")
("Exercises 41" "#41")
("Notes and References 45" "#45"))
)

Обратите внимание, что эта команда не будет работать, если два числа (например, 1 "и" # 1 ") различны или в этой строке больше номеров с этим шаблоном (например, 23" ... 32 "..." # 123 ") в одну строку.


ОБНОВИТЬ

Поскольку @Tim (OP) сказал, что число, за которым следует одна и та "же строка, может отличаться, я внес некоторые изменения в свое предыдущее решение и заставил его работать для вашего нового примера.

Кстати, из примера я чувствую, что это может быть таблица структуры контента, поэтому я не вижу, как эти два числа могут различаться. Сначала будет напечатан номер страницы, а 2 с # будет индекс страницы. Я прав?

Во всяком случае, вы знаете свое требование лучше всего. Теперь новое решение, все еще с gawk (я делю команду на строки, чтобы было легче читать):

awk 'BEGIN{FS=OFS="\" \"#"}{if(NF<2){print;next;}
        a=gensub(/.* ([0-9]+)$/,"\\1","g",$1);
        b=gensub(/([0-9]+)\"/,"\\1","g",$2); 
        gsub(/[0-9]+$/,a+11,$1);
        gsub(/^[0-9]+/,b+11,$2);
        print $1,$2
}' yourFile

протестируйте с вашим новым примером:

kent$  echo '(bookmarks
("Chapter 1 Introduction 1" "#1"
("1.1 Problem Statement and Basic Definitions 23" "#2")
("Exercises 31" "#30")
("Notes and References 42" "#34"))
)
'|awk 'BEGIN{FS=OFS="\" \"#"}{if(NF<2){print;next;}
        a=gensub(/.* ([0-9]+)$/,"\\1","g",$1);
        b=gensub(/([0-9]+)\"/,"\\1","g",$2); 
        gsub(/[0-9]+$/,a+11,$1);
        gsub(/^[0-9]+/,b+11,$2);
        print $1,$2
}'                        
(bookmarks
("Chapter 1 Introduction 12" "#12"
("1.1 Problem Statement and Basic Definitions 34" "#13")
("Exercises 42" "#41")
("Notes and References 53" "#45"))
)


EDIT2 на основе комментария @Tim

(1) Означает ли FS = OFS = "\" \ "#", что разделитель поля на входе и выходе - это двойная кавычка, пробел, двойная кавычка и #? Зачем указывать двойную кавычку дважды?

Вы правы для разделителя как во входной, так и в выходной части. Он определил разделитель как:

" "#

Есть две двойные кавычки, потому что легче поймать два числа, которые вы хотите (на основе вашего примера ввода).

(2) В /.* ([0-9] +) $ / означает ли $ конец строки?

Точно!

(3) В чем третий аргумент gensub (), в чем разница между "g" и "G"? нет разницы между G и g. Проверь это:

gensub(regexp, replacement, how [, target]) #
    Search the target string target for matches of the regular expression regexp. 
    If "how" is a string beginning with g or G (short for global”), then 
        replace all matches of regexp with replacement.

Это из http://www.gnu.org/s/gawk/manual/html_node/String-Functions.html . Вы можете прочитать, чтобы получить подробное использование gensub.


Благодарность! Интересно, как заставить это работать, если два числа, например, 1 "и" # 1 "разные?
Тим

этот ответ работает для вашего текущего требования / примера. если требование изменилось, возможно, вы могли бы отредактировать вопрос и привести лучший пример. и из вашего кода awk -F'#'кажется, что вы хотите вносить изменения только после '#'?
Кент

Спасибо за ваше предложение. Я просто изменил свой пример, чтобы два числа не совпадали.
Тим

@Tim см. Мой обновленный ответ, для вашего нового примера.
Кент

Благодарность! Некоторые вопросы: (1) FS=OFS="\" \"#"означает ли разделитель поля как на входе, так и на выходе двойные кавычки, пробел, двойные кавычки и #? зачем указывать двойную кавычку дважды? (2) в /.* ([0-9]+)$/, $означает ли конец строки? (3) в третьем аргументе gensub (), в чем разница между "g"и "G"?
Тим

7

В отличие от почти всех инструментов, которые предоставляют подстановки регулярных выражений, awk не допускает обратных ссылок, таких как \1текст замены. GNU Awk предоставляет доступ к соответствующим группам, если вы используете matchфункцию , но не с помощью ~или subили gsub.

Также обратите внимание, что даже если \1это поддерживается, ваш фрагмент будет добавлять строку +11, а не выполнять численные вычисления. Кроме того, ваше регулярное выражение не совсем верно, вы подходите к вещам вроде "42""и нет "#42".

Вот решение awk (предупреждение, не проверено). Он выполняет только одну замену на строку.

awk '
  match($0, /"#[0-9]+"/) {
    n = substr($0, RSTART+2, RLENGTH-3) + 11;
    $0 = substr($0, 1, RSTART+1) n substr($0, RSTART+RLENGTH-1)
  }
  1 {print}'

Было бы проще в Perl.

perl -pe 's/(?<="#)[0-9]+(?=")/$1+11/e'

Первое предложение вашего ответа именно то, что я искал. Однако тот факт, что вы сказали «... в тексте замены», вызывает дополнительный вопрос: разрешает ли awk обратные ссылки в самом шаблоне регулярных выражений?
Wildcard

1
@Wildcard Нет, awk просто не отслеживает группы (кроме упомянутого мной расширения GNU).
Жиль "ТАК - перестань быть злым"

5

awkможет сделать это, но это не напрямую, даже с использованием обратных ссылок.
GNU awk имеет (частичную) обратную ссылку в форме gensub .

Экземпляры 123"временно обертываются \x01и \x02помечаются как неизмененные (для sub(). Co

Или вы можете просто пройтись по циклу, изменяя кандидатов по ходу дела, в этом случае обратная ссылка и «скобки» не нужны; но отслеживание индекса символов необходимо.

awk '{$0=gensub(/([0-9]+)\"/, "\x01\\1\"\x02", "g", $0 )
      while ( match($0, /\x01[0-9]+\"\x02/) ) {
        temp=substr( $0, RSTART, RLENGTH )
        numb=substr( temp, 2, RLENGTH-3 ) + 11
        sub( /\x01[0-9]+\"\x02/, numb "\"" ) 
      } print }'

Вот еще один способ, используя gensubи массив splitи \x01как разделитель полей (для разделения ). \ X02 помечает элемент массива как кандидата на арифметическое сложение.

awk 'BEGIN{ ORS="" } {
     $0=gensub(/([0-9]+)\"/, "\x01\x02\\1\x01\"", "g", $0 )
     split( $0, a, "\x01" )
     for (i=0; i<length(a); i++) { 
       if( substr(a[i],1,1)=="\x02" ) { a[i]=substr(a[i],2) + 11 }
       print a[i]
     } print "\n" }'

Благодарность! В вашем первом коде (1) что это "\x01\\1\"\x02"значит? Я до сих пор не понимаю \x01и \x02. (2) насколько отличается возврат $0от gensubи $0как последний аргумент gensub?
Тим

@Тим. Шестнадцатеричные значения \x01и \x02используются в качестве маркеров замещения. Эти значения являются весьма маловероятно , чтобы быть в любом обычном текстовом файле, поэтому они одинаково «высоко» безопасно для использования (то есть. Не столкнуться столкновение с уже существующими) .. Они просто временные метки .. Re $0=gensub(... $0).. видеть это link String-Manipulation Functions , но в итоге: он (gensub) возвращает измененную строку в результате выполнения функции, и исходная целевая строка не изменяется. ... $0=Просто изменяет исходную цель ..
Peter.O

2

Поскольку решения в (g) awk кажутся довольно сложными, я хотел добавить альтернативное решение в Perl:

perl -wpe 's/\d+(?=")/$&+11/eg' < in.txt > out.txt

Объяснение:

  • Опция -wвключает предупреждения (которые будут предупреждать вас о возможных нежелательных эффектах).
  • Опция -pподразумевает цикл вокруг кода, который работает аналогично sed или awk, сохраняя каждую строку ввода автоматически в переменной по умолчанию $_.
  • Опция -eсообщает Perl, что программный код следует в командной строке, а не в файле сценария.
  • Код представляет собой regex substitution ( s/.../.../) $_, где последовательность цифр, если после нее следует буква a ", будет заменена на последовательность, интерпретируемую как число в сложении плюс 11.
  • В нулевой ширины положительное заключение упреждающая (?=pattern) ищет , "не принимая его в матче, так что мы не должны повторять его замены. Переменная MATCH $&в замене будет содержать только число.
  • /eМодификатор регулярного выражения говорит , perlчтобы «выполнить» замену в качестве кода вместо того , чтобы принимать его в виде строки.
  • /gМодификатор делает замену «глобальной», повторяя это на каждом матче в линии.

К $&сожалению, переменная MATCH отрицательно скажется на производительности кода в версиях Perl до 5.20. Более быстрое (и не намного более сложное) решение будет использовать $1вместо этого группирование и обратную ссылку :

perl -wpe 's/(\d+)?="/$1+11/eg' < in.txt > out.txt

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

perl -wpe 's/(\d+)"/$1+11 . q{"}/eg' < in.txt > out.txt
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.