Como usar regex com AWK para substituição de string?


13

Suponha que haja algum texto de um arquivo:

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

Eu quero adicionar 11 a cada número seguido por um "em cada linha, se houver um, ou seja,

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

Aqui está minha solução usando o GNU AWK e o regex:

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

ou seja, eu quero substituir (\d+)\"por \1+10\", onde \1está o grupo representando (\d+). Mas isso não funciona. Como posso fazer isso funcionar?

Se o gawk não é a melhor solução, o que mais pode ser usado?


Desculpe pela duplicação. Mas perguntei pela primeira vez no stackoverflow e não obtive resposta satisfatória, então sinalizei a migração. Mas isso não aconteceu por um tempo, então eu não esperava que isso acontecesse e perguntei no Unix.SE.
Tim

Respostas:


12

Tente isso (é necessário um gawk).

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

Teste com seu exemplo:

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"))
)

Observe que este comando não funcionará se os dois números (por exemplo, 1 "e" # 1 ") forem diferentes ou se houver mais números na mesma linha com esse padrão (por exemplo, 23" ... 32 "..." # 123 ") em uma linha.


ATUALIZAR

Desde @Tim (OP) disse que o número seguido por " mesma linha poderia ser diferente, fiz algumas alterações na minha solução anterior e a fiz funcionar no seu novo exemplo.

BTW, a partir do exemplo, sinto que poderia ser uma tabela de estrutura de conteúdo, então não vejo como os dois números podem ser diferentes. Primeiro seria o número da página impressa e o segundo com # seria o índice da página. Estou certo?

Enfim, você conhece melhor sua exigência. Agora a nova solução, ainda com gawk (eu quebro o comando em linhas para facilitar a leitura):

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

teste com seu novo exemplo:

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 com base no comentário de @Tim

(1) FS = OFS = "\" \ "#" significa que o separador de campo na entrada e na saída é aspas duplas, espaço, aspas duplas e #? Por que especificar aspas duplas duas vezes?

Você está certo para o separador na parte de entrada e saída. Definiu separador como:

" "#

Existem duas aspas duplas, porque é mais fácil capturar os dois números que você deseja (com base em sua entrada de exemplo).

(2) Em /.* ([0-9] +) $ /, $ significa o final da string?

Exatamente!

(3) No terceiro argumento de gensub (), qual é a diferença entre "g" e "G"? não há diferença entre G e g. Veja isso:

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.

Isto é de http://www.gnu.org/s/gawk/manual/html_node/String-Functions.html . você pode ler para obter um uso detalhado do gensub.


Obrigado! Gostaria de saber como fazê-lo funcionar se os dois números por exemplo, 1" e '# 1' são diferentes?
Tim

esta resposta funciona para o seu requisito / exemplo atual. se o requisito for alterado, talvez você possa editar a pergunta e dar um exemplo melhor. e pelo seu código awk -F'#', parece que você deseja fazer a alteração somente após o '#'?
Kent

Obrigado por sua sugestão. Acabei de modificar meu exemplo para que os dois números não sejam os mesmos.
Tim

@ Tim veja minha resposta atualizada, para seu novo exemplo.
Kent

Obrigado! Algumas perguntas: (1) FS=OFS="\" \"#"significa que o separador de campo na entrada e na saída é aspas duplas, espaço, aspas duplas e #? por que especificar aspas duplas duas vezes? (2) in /.* ([0-9]+)$/, $significa o fim da string? (3) no terceiro argumento de gensub (), qual é a diferença entre "g"e "G"?
Tim

7

Ao contrário de quase todas as ferramentas que fornecem substituições regexp, o awk não permite referências posteriores, como \1no texto de substituição. O GNU Awk dá acesso a grupos correspondentes se você usar a matchfunção , mas não com ~ou subou gsub.

Observe também que, mesmo se tiver \1sido suportado, seu snippet acrescentará a string +11, não executará um cálculo numérico. Além disso, seu regexp não está certo, você está combinando coisas como essas "42""e não "#42".

Aqui está uma solução awk (aviso, não testado). Ele executa apenas uma única substituição por linha.

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}'

Seria mais simples no Perl.

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

A primeira frase da sua resposta é exatamente o que eu estava procurando. No entanto, o fato de você ter dito "... no texto de substituição" levanta uma questão de acompanhamento: o awk permite referências posteriores no próprio padrão regex?
Curinga

1
@Wildcard Não, o awk simplesmente não controla grupos (exceto a extensão GNU mencionada).
Gilles 'SO- stop be evil'

5

awkpode fazê-lo, mas não é direto, mesmo usando backreferencing.
O GNU awk possui um registro posterior (parcial), na forma de gensub .

As instâncias de 123"são temporariamente envolvidas \x01e \x02marcadas como não modificadas (para sub(). Co

Ou você pode simplesmente passar pelo processo de mudança de candidatos à medida que avança; nesse caso, a referência posterior e os "colchetes" não são necessários; mas é necessário acompanhar o índice de caracteres.

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 }'

Aqui está outra maneira, usando gensube array splite \x01como um delimitador de campo (para divisão ). \ X02 marca um elemento do array como candidato à adição aritmética.

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" }'

Obrigado! No seu primeiro código, (1) o que "\x01\\1\"\x02"significa? Eu ainda não entendo \x01e \x02. (2) quão diferente é o retorno $0de gensube $0como o último argumento para gensub?
Tim

@Tim. Os valores hexadecimais \x01e \x02são usados ​​como marcadores de substituição. Estes valores são altamente improvável que seja em qualquer normal de arquivo de texto, para que eles são igualmente "muito" seguro de usar (ie. Não encontrar um confronto com os pré-existentes) .. Eles são apenas rótulos temporários .. Re $0=gensub(... $0).. ver isso link Funções de manipulação de string , mas em resumo: (gensub) retorna a string modificada como resultado da função e a string de destino original não é alterada. ... A $0=simplesmente modifica o alvo original ..
Peter.O

2

Como as soluções em (g) awk parecem se tornar bastante complexas, eu queria adicionar uma solução alternativa no Perl:

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

Explicação:

  • A opção -wativa avisos (que avisam sobre possíveis efeitos indesejados).
  • Option -pimplica um loop em torno do código que funciona de forma semelhante a sed ou awk, salvando cada linha de entrada automaticamente na variável padrão $_,.
  • A opção -ediz ao perl que o código do programa está seguindo na linha de comando, não em um arquivo de script.
  • O código é uma substituição de regex ( s/.../.../) em $_, onde uma sequência de dígitos, se for seguida por a ", será substituída pela sequência, interpretada como um número na adição, mais 11.
  • A afirmação de antecipação positiva de largura zero (?=pattern) procura a "sem levá-la para a partida, portanto não precisamos repeti-la na substituição. A variável MATCH $&na substituição conterá apenas o número.
  • O /emodificador para o regex diz perlpara "executar" a substituição como código em vez de tomá-la como uma string.
  • O /gmodificador torna a substituição "global", repetindo-a em todas as correspondências da linha.

$&Infelizmente, a variável MATCH prejudicará o desempenho do código nas versões Perl anteriores à 5.20. Uma solução mais rápida (e não muito mais complexa) usaria o agrupamento e a referência anterior $1:

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

E se a afirmação antecipada parecer muito confusa, você também poderá substituir as aspas explicitamente:

perl -wpe 's/(\d+)"/$1+11 . q{"}/eg' < in.txt > out.txt
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.