¿Cómo usar regex con AWK para el reemplazo de cadenas?

13

Supongamos que hay texto de un archivo:

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

Quiero agregar 11 a cada número seguido de un "en cada línea si hay uno, es decir

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

Aquí está mi solución usando GNU AWK y regex:

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

es decir, quiero reemplazar (\d+)\"con \1+10\", donde \1representa el grupo (\d+). Pero no funciona. ¿Cómo puedo hacer que funcione?

Si gawk no es la mejor solución, ¿qué más se puede usar?

Tim
fuente
Perdón por la duplicación. Pero primero pregunté por stackoverflow, y no obtuve una respuesta satisfactoria, así que marqué para la migración. Pero no sucedió por un tiempo, así que no esperaba que sucediera y luego pregunté por Unix.SE.
Tim

Respuestas:

12

Prueba esto (se necesita gawk).

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

Prueba con tu ejemplo:

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

Tenga en cuenta que este comando no funcionará si los dos números (por ejemplo, 1 "y" # 1 ") son diferentes, o si hay más números en la misma línea con este patrón (por ejemplo, 23" ... 32 "..." # 123 ") en una línea.


ACTUALIZAR

Como @Tim (OP) dijo que el número seguido de la "misma línea podría ser diferente, hice algunos cambios en mi solución anterior y lo hice funcionar para su nuevo ejemplo.

Por cierto, del ejemplo siento que podría ser una tabla de estructura de contenido, por lo que no veo cómo los dos números podrían ser diferentes. Primero sería el número de página impresa, y segundo con # sería el índice de la página. Estoy en lo cierto?

De todos modos, conoces mejor tus requisitos. Ahora la nueva solución, todavía con gawk (divido el comando en líneas para que sea más fácil de leer):

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

prueba con tu nuevo ejemplo:

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 basado en el comentario de @Tim

(1) ¿FS = OFS = "\" \ "#" significa que el separador de campo tanto en la entrada como en la salida es comillas dobles, espacios, comillas dobles y #? ¿Por qué especificar comillas dobles dos veces?

Tiene razón para el separador tanto en la parte de entrada como en la de salida. Definió separador como:

" "#

Hay dos comillas dobles, porque es más fácil capturar los dos números que desea (según su entrada de ejemplo).

(2) En /.* ([0-9] +) $ /, ¿$ significa el final de la cadena?

¡Exactamente!

(3) En el tercer argumento de gensub (), ¿cuál es la diferencia entre "g" y "G"? no hay diferencia entre G y g. Mira esto:

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.

Esto es de http://www.gnu.org/s/gawk/manual/html_node/String-Functions.html . puede leer para obtener un uso detallado de gensub.

Kent
fuente
¡Gracias! Me pregunto cómo hacer que funcione si los dos números, por ejemplo, 1 "y" # 1 "son diferentes
Tim
esta respuesta funciona para su requisito / ejemplo actual. Si se cambia el requisito, tal vez podría editar la pregunta y dar un mejor ejemplo. y de su código awk -F'#', parece que solo quiere hacer el cambio en la parte después del '#'?
Kent
Gracias por tu sugerencia. Acabo de modificar mi ejemplo para que los dos números no sean iguales.
Tim
@Tim vea mi respuesta actualizada, para su nuevo ejemplo.
Kent
¡Gracias! Algunas preguntas: (1) FS=OFS="\" \"#"significa que el separador de campo tanto en la entrada como en la salida es comillas dobles, espacios, comillas dobles y #? ¿Por qué especificar comillas dobles dos veces? (2) en /.* ([0-9]+)$/, ¿ $significa el final de la cadena? (3) en el tercer argumento de gensub (), ¿cuál es la diferencia entre "g"y "G"?
Tim
7

A diferencia de casi todas las herramientas que proporcionan sustituciones regexp, awk no permite referencias posteriores como \1en el texto de reemplazo. GNU Awk da acceso a grupos coincidentes si usa la matchfunción , pero no con ~o subo gsub.

Tenga en cuenta también que incluso si \1fuera compatible, su fragmento agregaría la cadena +11, no realizaría un cálculo numérico. Además, tu expresión regular no es del todo correcta, estás combinando cosas como "42""y no "#42".

Aquí hay una solución awk (advertencia, no probada). Solo realiza un solo reemplazo por línea.

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

Sería más simple en Perl.

perl -pe 's/(?<="#)[0-9]+(?=")/$1+11/e'
Gilles 'SO- deja de ser malvado'
fuente
La primera oración de tu respuesta es exactamente lo que estaba buscando. Sin embargo, el hecho de que haya dicho "... en texto de reemplazo" plantea una pregunta de seguimiento: ¿permite awk hacer referencias en el patrón de expresiones regulares?
Comodín el
1
@Wildcard No, awk simplemente no realiza un seguimiento de los grupos (excepto por la extensión GNU que menciono).
Gilles 'SO- deja de ser malvado'
5

awkpuede hacerlo, pero no es directo, incluso utilizando referencias inversas.
GNU awk tiene referencias parciales (parciales), en forma de gensub .

Las instancias de 123"se envuelven temporalmente \x01y se \x02marcan como no modificadas (for sub(). Co

O simplemente puede pasar por el ciclo cambiando candidatos a medida que avanza, en cuyo caso, no se necesitan referencias y "paréntesis"; pero se necesita hacer un seguimiento del í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 }'

Aquí hay otra forma, usando gensuby array splity \x01como delimitador de campo (para división ) ... \ x02 marca un elemento de matriz como candidato para la suma 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" }'
Peter.O
fuente
¡Gracias! En su primer código, (1) ¿qué "\x01\\1\"\x02"significa? Todavía no entiendo \x01y \x02. (2) ¿cuán diferente es el retorno $0por gensuby el $0como último argumento gensub?
Tim
@Tim. Los valores hexadecimales \x01y \x02se utilizan como marcadores de sustitución. Estos valores son altamente improbable que sea, en cualquier normales archivo de texto, por lo que son igualmente "muy" seguro de usar (es decir. No encontrará un enfrentamiento con los ya existentes) .. Son simplemente etiquetas temporales .. Re $0=gensub(... $0).. ver esto enlace Funciones de manipulación de cadenas , pero en resumen: (gensub) devuelve la cadena modificada como resultado de la función y la cadena de destino original no cambia. ... El $0=simplemente modifica el objetivo original ..
Peter.O
2

Dado que las soluciones en (g) awk parecen volverse bastante complejas, quería agregar una solución alternativa en Perl:

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

Explicación:

  • La opción -whabilita advertencias (que le advertirán de posibles efectos no deseados).
  • Opción -pimplica un lazo alrededor del código que funciona de forma similar a la sed o awk, ahorrando cada línea de entrada de forma automática en la variable por defecto, $_.
  • La opción -ele dice a Perl que el código del programa sigue en la línea de comando, no en un archivo de script.
  • El código es una sustitución de expresiones regulares ( s/.../.../) en $_, donde una secuencia de dígitos, si es seguida por una ", será reemplazada por la secuencia, interpretada como un número en la suma, más 11.
  • La afirmación de anticipación positiva de ancho cero (?=pattern) busca el "sin tomarlo en el partido, por lo que no tenemos que repetirlo en el reemplazo. La variable MATCH $&en el reemplazo contendrá solo el número.
  • El /emodificador de la expresión regular le dice perla "ejecutar" el reemplazo como código en lugar de tomarlo como una cadena.
  • El /gmodificador hace el reemplazo "global", repitiéndolo en cada partido en la línea.

$&Desafortunadamente, la variable MATCH será perjudicial para el rendimiento del código en las versiones de Perl anteriores a 5.20. Una solución más rápida (y no mucho más compleja) usaría la agrupación y la referencia inversa $1en su lugar:

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

Y si la afirmación de anticipación parece demasiado confusa, también podría reemplazar la comilla explícitamente:

perl -wpe 's/(\d+)"/$1+11 . q{"}/eg' < in.txt > out.txt
Dubu
fuente