Reemplazo de patrón de coincidencia de casos con sed

14

Tengo un código fuente distribuido en varios archivos.

  • Tiene un patrón con el abcdefque necesito reemplazar pqrstuvxyz.
  • El patrón podría ser Abcdef(Sentence Case) y luego debe ser reemplazado por Pqrstuvxyz.
  • El patrón podría ser AbCdEf(caso de alternar) y luego debe ser reemplazado por PqRsTuVxYz.

En resumen, necesito hacer coincidir el caso del patrón de origen y aplicar el patrón de destino apropiado.

¿Cómo puedo lograr esto usando sedo cualquier otra herramienta?

usuario1263746
fuente
¿Y si es así ABcDeF?
Stéphane Chazelas
PQrStUvxyz: entiendo tu punto.
user1263746
Entonces, si ABcDeF-> PQrStUvxyz, entonces seguramente AbCdEf-> PqRsTuvxyzsería lógicamente consistente. Si el caso se va a copiar de una cadena a la otra, ¿qué debería suceder si la segunda cadena de reemplazo es más larga?
Graeme
Bueno, recortemos el reemplazo de "pqrstu" por razones de brevedad.
user1263746

Respuestas:

9

Solución portátil con sed:

sed '
:1
/[aA][bB][cC][dD][eE][fF]/!b
s//\
&\
pqrstu\
PQRSTU\
/;:2
s/\n[[:lower:]]\(.*\n\)\(.\)\(.*\n\).\(.*\n\)/\2\
\1\3\4/;s/\n[^[:lower:]]\(.*\n\).\(.*\n\)\(.\)\(.*\n\)/\3\
\1\2\4/;t2
s/\n.*\n//;b1'

Es un poco más fácil con GNU sed:

search=abcdef replace=pqrstuvwx
sed -r ":1;/$search/I!b;s//\n&&&\n$replace\n/;:2
    s/\n[[:lower:]](.*\n)(.)(.*\n)/\l\2\n\1\3/
    s/\n[^[:lower:]](.*\n)(.)(.*\n)/\u\2\n\1\3/;t2
    s/\n.*\n(.*)\n/\1/g;b1"

Al usar lo &&&anterior, reutilizamos el patrón de mayúsculas y minúsculas de la cadena para el resto del reemplazo, por lo ABcdefque se cambiaría a PQrstuVWxy AbCdEfpor PqRsTuVwX. Cambie a &para afectar solo el caso de los primeros 6 caracteres.

(tenga en cuenta que puede no hacer lo que desea o puede encontrarse con un bucle infinito si el reemplazo puede estar sujeto a sustitución (por ejemplo, si se sustituye foopor foo, o bcdpor abcd)

Stéphane Chazelas
fuente
8

Solución portátil con awk:

awk -v find=abcdef -v rep=pqrstu '{
  lwr=tolower($0)
  offset=index(lwr, tolower(find))

  if( offset > 0 ) {
    printf "%s", substr($0, 0, offset)
    len=length(find)

    for( i=0; i<len; i++ ) {
      out=substr(rep, i+1, 1)

      if( substr($0, offset+i, 1) == substr(lwr, offset+i, 1) )
        printf "%s", tolower(out)
      else
        printf "%s", toupper(out)
    }

    printf "%s\n", substr($0, offset+len)
  }
}'

Entrada de ejemplo:

other abcdef other
other Abcdef other
other AbCdEf other

Salida de ejemplo:

other pqrstu other
other Pqrstu other
other PqRsTu other

Actualizar

Como se señaló en los comentarios, lo anterior solo reemplazará la primera instancia de findcada línea. Para reemplazar todas las instancias:

awk -v find=abcdef -v rep=pqrstu '{
  input=$0
  lwr=tolower(input)
  offset=index(lwr, tolower(find))

  if( offset > 0 ) {
    while( offset > 0 ) {

      printf "%s", substr(input, 0, offset)
      len=length(find)

      for( i=0; i<len; i++ ) {
        out=substr(rep, i+1, 1)

        if( substr(input, offset+i, 1) == substr(lwr, offset+i, 1) )
          printf "%s", tolower(out)
        else
          printf "%s", toupper(out)
      }

      input=substr(input, offset+len)
      lwr=substr(lwr, offset+len)
      offset=index(lwr, tolower(find))
    }

    print input
  }
}'

Entrada de ejemplo:

other abcdef other ABCdef other
other Abcdef other abcDEF
other AbCdEf other aBCdEf other

Salida de ejemplo:

other pqrstu other PQRstu other
other Pqrstu other pqrSTU
other PqRsTu other pQRsTu other
Graeme
fuente
Tenga en cuenta que solo procesa una instancia por línea.
Stéphane Chazelas
@StephaneChazelas, actualizado para manejar múltiples instancias.
Graeme
6

Podrías usar perl. Directamente de las preguntas frecuentes - citando perldoc perlfaq6:

¿Cómo puedo sustituir mayúsculas y minúsculas en el LHS mientras se preserva el caso en el RHS?

Aquí hay una hermosa solución Perlish de Larry Rosler. Explota las propiedades de bit xor en cadenas ASCII.

   $_= "this is a TEsT case";

   $old = 'test';
   $new = 'success';

   s{(\Q$old\E)}
   { uc $new | (uc $1 ^ $1) .
           (uc(substr $1, -1) ^ substr $1, -1) x
           (length($new) - length $1)
   }egi;

   print;

Y aquí está como una subrutina, modelada a partir de lo anterior:

       sub preserve_case($$) {
               my ($old, $new) = @_;
               my $mask = uc $old ^ $old;

               uc $new | $mask .
                       substr($mask, -1) x (length($new) - length($old))
   }

       $string = "this is a TEsT case";
       $string =~ s/(test)/preserve_case($1, "success")/egi;
       print "$string\n";

Esto imprime:

           this is a SUcCESS case

Como alternativa, para mantener el caso de la palabra de reemplazo si es más larga que la original, puede usar este código, por Jeff Pinyan:

   sub preserve_case {
           my ($from, $to) = @_;
           my ($lf, $lt) = map length, @_;

           if ($lt < $lf) { $from = substr $from, 0, $lt }
           else { $from .= substr $to, $lf }

           return uc $to | ($from ^ uc $from);
           }

Esto cambia la oración a "este es un caso de ÉXITO".

Solo para mostrar que los programadores de C pueden escribir C en cualquier lenguaje de programación, si prefiere una solución más similar a C, el siguiente script hace que la sustitución tenga el mismo caso, letra por letra, que el original. (También sucede que se ejecuta aproximadamente un 240% más lento que la solución Perlish). Si la sustitución tiene más caracteres que la cadena que se sustituye, el caso del último carácter se utiliza para el resto de la sustitución.

   # Original by Nathan Torkington, massaged by Jeffrey Friedl
   #
   sub preserve_case($$)
   {
           my ($old, $new) = @_;
           my ($state) = 0; # 0 = no change; 1 = lc; 2 = uc
           my ($i, $oldlen, $newlen, $c) = (0, length($old), length($new));
           my ($len) = $oldlen < $newlen ? $oldlen : $newlen;

           for ($i = 0; $i < $len; $i++) {
                   if ($c = substr($old, $i, 1), $c =~ /[\W\d_]/) {
                           $state = 0;
                   } elsif (lc $c eq $c) {
                           substr($new, $i, 1) = lc(substr($new, $i, 1));
                           $state = 1;
                   } else {
                           substr($new, $i, 1) = uc(substr($new, $i, 1));
                           $state = 2;
                   }
           }
           # finish up with any remaining new (for when new is longer than old)
           if ($newlen > $oldlen) {
                   if ($state == 1) {
                           substr($new, $oldlen) = lc(substr($new, $oldlen));
                   } elsif ($state == 2) {
                           substr($new, $oldlen) = uc(substr($new, $oldlen));
                   }
           }
           return $new;
   }
devnull
fuente
Tenga en cuenta que está limitado a letras ASCII.
Stéphane Chazelas
5

Si recorta el reemplazo para pqrstu, intente esto:

Entrada:

abcdef
Abcdef
AbCdEf
ABcDeF

Ouput:

$ perl -lpe 's/$_/$_^lc($_)^"pqrstu"/ei' file
pqrstu
Pqrstu
PqRsTu
PQrStU

Si desea reemplazar con prstuvxyz, puede ser esto:

$ perl -lne '@c=unpack("(A4)*",$_);
    $_ =~ s/$_/$_^lc($_)^"pqrstu"/ei;
    $c[0] =~ s/$c[0]/$c[0]^lc($c[0])^"vxyz"/ei;
    print $_,$c[0]' file
pqrstuvxyz
PqrstuVxyz
PqRsTuVxYz
PQrStUVXyZ

No puedo encontrar ninguna regla para mapear ABcDeF-> PQrStUvxyz.

Cuonglm
fuente
Tenga en cuenta que está limitado a letras ASCII.
Stéphane Chazelas
3

Algo como esto haría lo que describiste.

sed -i.bak -e "s/abcdef/pqrstuvxyz/g" \
 -e "s/AbCdEf/PqRsTuVxYz/g" \
 -e "s/Abcdef/Pqrstuvxyz/g" files/src
UnX
fuente