# [¿sed/awk/grep?] Extraer texto de un fichero sin formato

## Stolz

Versión corta

¿Cómo puedo extraer de un fichero de texto sin formato aparente todas las frases que cumplan un cierto patrón? Con sin formato  me refiero a que las frases a extraer pueden tener cualquier longitud y puede haber más de una frase por línea.

La expresión regular que define el patrón ya la tengo, me falta averiguar el comando (o combinación de comandos) que me muestre solo las partes coincidentes, no toda la línea. Parece que awk es para ficheros con formato, sed es más bien para reemplazos, y grep me muestra resaltados justo lo que quiero pero no omite el resto de la salida.

Versión larga

Me ha tocado hacerme cargo de una web que hay que traducir. En un intento inicial de traducción el autor original encerró todas las frases susceptibles de ser traducidas de la forma que exige GNU Gettext, es decir así: _('Frase a traducir'). En principio las herramientas de gettext permiten extraer las cadenas del código que cumplan esa característica sin mayor problema. El problema es que el autor original no entendió muy bien la documentación de GNU gettext o se olvidó de leer la parte en la que se dice que el idioma original debe tener codificación ASCII y no se le ocurrió mejor idea que hacer la web original en portugués y UTF8.  Al intentar usar las herramientas de gettext fallan en cuanto encuentran un carácter especial (tilde,diéresis o similar) y el portugués está lleno de ellos. Para no realizar la sustitución/traducción a mano había pensado en automatizarlo con la línea de comandos, ya que cientos de frases re repiten una y otra vez en el código y mi nivel de portugués es mas bien ínfimo.

Un ejemplo del código:

```
echo '<tr><td>'._('Herança do valor.').'</td></tr><tr><td>'._('Repartição do valor.').'</td></tr>';
```

y lo que quiero consegur es algo así:

```
Herança do valor.

Repartição do valor.

```

aunque me conformaría con cualquier cosa parecida, como:

```
Herança do valor.  Repartição do valor.
```

o incluso 

```
_('Herança do valor.')

_('Repartição do valor.')
```

Ya tengo una expresión regular (aunque se puede mejorar) que hace que grep me muestre coloreada justo la parte que quiero extraer, pero como he dicho, me falta averiguar el comando (o combinación de comandos) que me muestre solo las partes coincidentes, no toda la línea.

Si ejecuto lo siguiente:

```
grep -E --color "_\('[a-z ]+'\)" archivo.php
```

me devuelve  *Quote:*   

> echo '<tr><td>'._('Herança do valor.').'</td></tr><tr><td>'._('Repartição do valor.').'</td></tr>';

 

Pero a partir de ahí no se como continuar. Había pensado usar sed para reemplazar todo lo que no coincida con mi expresión y continuar a partir de ahí pero no he sabido hacerlo

¿alguna sugerencia? ¿existe alguna otra herramientas para estos menesteres?

----------

## achaw

Hace un tiempo estuve trabajando en un proyecto personal en la cual se me presento una solucion similar, actualmente...no recuerdo bien como lo maneje, pero quizas esto que arme te sirva de punto de partida. Suponiendo que tenemos el archivo n que contiene la linea que mencionas:

```
echo '<tr><td>'._('Herança do valor.').'</td></tr><tr><td>'._('Repartição do valor.').'</td></tr>';
```

Este es el script que cree:

```
#!/bin/bash

archivo=$1

linea1=`grep "Herança do valor" $archivo | sed -e s/"echo '<tr><td>'._('"// -e s/".').'<\/td><\/tr><tr><td>'._('Repartição do valor.').'<\/td><\/tr>';"//`

linea2=`grep "Herança do valor" $archivo | sed -e s/"echo '<tr><td>'._('Herança do valor.').'<\/td><\/tr><tr><td>'._('"// -e s/".').'<\/td><\/tr>';"//`

echo "$linea1

$linea2"
```

Lo que hace el scritp es localizar la linea y filtrar todo lo que no te interese con sed. Fijate que es simple, seguramente lo podes perfeccionar muchisimo, filtrando caracteres o frases comunes. Un detalle ante una slash de este tipo /, anteponer una invertida \, sino sed fallara con errores de sintaxis. Lo llamas de esta forma:

```
achaw@localhost ~ $ ./reemp n
```

En este caso llame al script reemp, y al archivo con textos n.

Seguro puse muchas cosas que ya sabias, es que ando con poco tiempo...Espero que te sirva.

Saludos

----------

## Coghan

Creo que con simples cortes de bash se puede hacer:

```
#!/usr/bin/env bash

VAR=$(cat archivo.php)

CUT1=${VAR#*_(\'}

CUT2=${CUT1%%\')*}

CUT3=${VAR##*_(\'}

CUT4=${CUT3%\')*}

echo ${CUT2}

echo ${CUT4}

```

----------

## Stolz

Gracias por el interés achaw, pero me temo que no me sirve. La solución que propones requiere conocer de antemano todas las frase posibles y ese es precisamente el probelma que trato de resolver. Además requiere una línea de script por cada posible frase y si suponemos que hay una frase cada 5 líneas de código estaríamos hablando de más de 1500 líneas de script :/

----------

## AnimAlf

Hola amigos,

los que querais aprender más sobre sed no os podeis perder en este dominio "Sed mediante ejemplos".

Me estoy intentando empapar en ello y he pensado que también le puede interesar a alguien más, y así aprovecho el hilo   :Smile:   que desde luego no tiene desperdicio   :Rolling Eyes: 

Saludos

PD: ahora estoy oxidado en esto, pero si rodeas entre parentesis lo que quieres extraer, luego lo puedes extraer con $n  donde n sería 1 para el primer parentesis 2 para el segundo ... pero, no lo recuerdo bien ahora (y era más bien en perl), algo así como "_\(('[a-z ]+)'\)" con lo que hay entre parentesis es lo que te interesa.

----------

## achaw

```
#!/bin/bash

archivo=$1

linea1=`grep "_(" $archivo | sed -e s/"echo '<tr><td>'._('"// -e s/"').'<\/td><\/tr><tr><td>'._('"/'    '/ -e s/"').'<\/td><\/tr>';"//`

echo "$linea1"
```

Suponiendo que el patron es _( y las lineas son todas iguales a diferencia de los strings que contienen.

----------

## Coghan

Este script te lista las líneas que concuerden con el patron _(' que se supone que contienen el texto a extraer dentro del fichero archivo.php y luego corta el contenido de la linea sobrante dejando solo el texto uno debajo del otro por cada linea encontrada como pedías.

```
#!/bin/bash

IFS=$'\n'

for VAR in $(grep '_('\' archivo.php);do

IFS=" "

CUT1=${VAR#*_(\'}

CUT2=${CUT1%%\')*}

CUT3=${VAR##*_(\'}

CUT4=${CUT3%\')*} 

echo ${CUT2} 

echo ${CUT4} 

done

```

----------

## Stolz

De nuevo gracias por el interés achaw pero como he dicho el archivo no tiene formato, es decir,  no son todas las lineas iguales (sin contar el texto a extraer). Es una página web (muchos ficheros PHP con cientos de líneas distintas).

Coghan, no vi tu primera respuesta porque fue casi simultanea a la mía. He probado la segunda versión  y aunque no entiendo el código funciona aceptablemente bien. Omite algunas coincidencias en las líenas con más de una y da algunos duplicados pero con sort | uniq los quitaré. Muchas gracias.

Llamarme animal pero me ha dado por recordar mis inicios en la programación y me había puesto a hacer un programa en C. Ya tenía casi acabada la versión inicial (funciona solo para la primera coincidencia de una línea). Aquí la dejo por si a alguien le interesa un ejemplo sencillo del uso de expresiones regulares en C.

```
/*

   Extrae las partes de un fichero de texto que coinciden con las expresiones regulares indicadas

   en el vector 'expresiones_regulares'

   Autor: Stolz ( http://stolz.gsmlandia.com )

   This program is free software: you can redistribute it and/or modify

   it under the terms of the GNU General Public License as published by

   the Free Software Foundation, either version 3 of the License, or

   (at your option) any later version.

   This program is distributed in the hope that it will be useful,

   but WITHOUT ANY WARRANTY; without even the implied warranty of

   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the

   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License

   along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <regex.h>

#define TAM_BUF 1024

#define SIZEMATCH(match)(match.rm_eo - match.rm_so)

int main(int argc, char *argv[])

{

   if(argc < 2)

   {

      fprintf(stderr,"Debes indicar un archivo como parametro\n");

      exit(EXIT_FAILURE);

   }

   //Archivo que contiene el texto que vamos a buscar con las nuestras expresiones regulares

   FILE *origen;

   if((origen=fopen(argv[1], "r"))==NULL)

   {

      perror("Error abriendo fichero origen: ");

      exit(EXIT_FAILURE);

   }

   //Vector de expresiones regulares a buscar

   const char *expresiones_regulares[]={

      "_\\('[a-z ]+'\\)",

      };

   //Número de elementos del vector de expresiones regulares

   const int num_elem_regexs = 1;

   //Almacén para la versión binaria de las expresiones regulares

   regex_t expresiones_regulares_compiladas[num_elem_regexs];

   //Variable para almacenar el código que se produce al compilar una ER

   int error_regcomp = 0;

   //Buffer para leer el fichero origen (y mostrar los mensajes de error si se producen)

   char buff[TAM_BUF];

   //Compilamos las expresiones regulares a un formato adecuado para ser usado por regexec

   int i;

   for(i = 0 ; i < num_elem_regexs; i++)

   {

      /*Hay que 'compilarlar' la ER con la funcion regcomp(&expresiones_regulares_compiladas, expresiones_regulares, flags)

      los flag pueden ser:

         REG_EXTENDED : Utilizar el tipo de sintaxis 'Expresión Regular Extendida POSIX' cuando se interprete regex. Si no se activa esta opción, se utiliza el tipo de sintaxis 'Expresión Regular Básica POSIX'.

         REG_ICASE : No distinguir entre mayúsculas y minúsculas.

         REG_NOSUB : No se necesita soporte para obtener las posiciones de subcadenas coincidentes. Si se indica este flag los parámetros nmatch y pmatch indicados al invocar a regexec() se ignoraran

         REG_NEWLINE : Con operadores 'comodín' y gloables(por ejemplo -*,+,^,$) el salto de linea no se considera una coincidencia */

      error_regcomp = regcomp(&expresiones_regulares_compiladas[i], expresiones_regulares[i] , REG_ICASE | REG_NEWLINE | REG_EXTENDED);

      if(error_regcomp != 0)

      {

         regerror(error_regcomp, &expresiones_regulares_compiladas[i], buff, sizeof(buff));

         fprintf(stderr,"Error compilando la expresion '%s': %s\n",expresiones_regulares[i],buff);

         exit(EXIT_FAILURE);

      }

   }

   //Variables para almacenar la posición de las posibles coincidencias

   int nmatch = 3;

   regmatch_t pmatch[nmatch]; //regmatch_t es una estructura con dos campos: 'rm_so' que si no es -1 indica el la posición inicial de la coincidencia y 'rm_eo' que indica la posición final de la coincidencia

   //Leemos línea a línea el fichero origen

   int terminar = 0;

   while(!terminar)

   {

      //Leemos una linea

      if(fgets(buff, TAM_BUF, origen))

      {

         //hay línea, buscamos si coincide con alguna de nuestras expresiones regulares

         for(i = 0 ; i < num_elem_regexs; i++)

         {

            if(regexec(&expresiones_regulares_compiladas[i],buff , nmatch, pmatch, 0) == 0)

            {

               //Hay coincidencia, la mostramos por pantalla

               //printf("Coincidencia de %d a %d.\n",pmatch[0].rm_so, pmatch[0].rm_eo);

               int j;

                for(j=0; j <= expresiones_regulares_compiladas[i].re_nsub; j++)

                {

                  if(pmatch[j].rm_so != -1)

                  {

                     size_t matchlen = SIZEMATCH(pmatch[j]);

                     char * submatch;

                     submatch =(char *) malloc(matchlen+1);

                     strncpy(submatch,buff+pmatch[j].rm_so,matchlen);

                     submatch[matchlen] = '\0';

                     printf("Coincidencia con ER %i: %s\n",i+1,submatch);

                     free(submatch);

                  }

               }

            }

         }

      }

      else

         /* EOF ? */

         terminar = 1;

   }

   //Liberamos memoria

   for(i = 0 ; i < num_elem_regexs; i++)

   {

      regfree(&expresiones_regulares_compiladas[i]);

   }

   return EXIT_SUCCESS;

}

```

¡Me ha costado más de una hora hacerlo porque entre que llevaba años sin tocar C y que nunca había programado nada con expresiones regulares!. Total para que dentro de unas horas aparezca i92guboj y nos diga que se podía haber hecho con con una sola línea de  perl  :Very Happy: 

Dejo el hijo abierto a la espera de una solución mejor.

----------

## achaw

Bueno, lo importante es que funcione. Yo intentaba matar moscas a escopetazos  :Very Happy: 

Saludos

EDITO

_____

En ningun momento entendi lo de "sin formato" hasta ahora jejeje...eso es por no pensar lo que leo   :Embarassed: 

----------

## Coghan

 *Stolz wrote:*   

> Coghan, no vi tu primera respuesta porque fue casi simultanea a la mía. He probado la segunda versión  y aunque no entiendo el código funciona aceptablemente bien. Omite algunas coincidencias en las líenas con más de una y da algunos duplicados pero con sort | uniq los quitaré. Muchas gracias.

 

Intentaré explicar lo mejor que pueda. No soy programador, solo un simple aficionado que se ha leído este manual http://www.gentoo.org/doc/es/articles/bash-by-example-p1.xml y sucesivos así como la página man de bash sobre todo para poder crear mis primeros ebuild.

He modificado y simplificado el script para que encuentre todas las coincidencias en una misma línea y no duplique. La única pega es que me deja en la última línea un resto, espero que no te importe.

```
#!/bin/bash

IFS=$')'

for texto in $(grep '_('\' archivo.php); do

corte=${texto#*_(\'}

echo ${corte%\'*}         

done
```

En la primera línea cambio una variable interna de Bash IFS esta se usa para indicar el delimitador de campo, por defecto es un espacio y lo cambio para que sea el cierre de paréntesis como tal, de esa manera en el final de cada campo estará el texto que necesitamos, solo queda filtrar la basura restante.

Lo siguiente creo un bucle para que lea el archivo.php y solo liste las líneas que contengan esta cadena " _(' " que se supone y según la referencia que das deben contener las líneas de texto a extraer.

Y el resto es solo cortar cadenas siguiendo la sección 1.17 del manual que indico arriba es fácil de entender. Primero corta desde el comienzo del campo hasta el final de la famosa cadena _(' (a partir de aquí es donde comienza el texto que nos interesa y luego imprimo el resto con el corte del final del campo que no nos interesa delimitado por ') hasta el final del campo.

Espero que te sirva de ayuda.   :Wink: 

----------

## opotonil

Si te vale en PHP, que ultimamente es lo que mas he utilizado, el ejemplo que pusiste me funciona simplemente con:

```

<?php

ini_set("default_charset", "UTF-8");

mb_internal_encoding(ini_get("default_charset"));

mb_regex_encoding(mb_internal_encoding());

$string = "<tr><td>'._('Herança do valor.').'</td></tr><tr><td>'._('Repartição do valor.').'</td></tr>";

$array = array();

mb_eregi("'\._\('([[:alnum:] \.]+)'\)\.'.+'\._\('([[:alnum:] \.]+)'\)\.'", $string, $array);

print_r($array);

?>

```

Resultado:

```

Array

(

    [0] => '._('Herança do valor.').'</td></tr><tr><td>'._('Repartição do valor.').'

    [1] => Herança do valor.

    [2] => Repartição do valor.

)

```

Salu2.

PD: despues de leer un poco mejor el post puede que fuera mejor "preg_match_all" pero no se que tal se comporta con UTF-8 y no se si conoces PHP asi que por si acaso con "file" (http://es.php.net/manual/es/function.file.php) lees un fichero a un array, siendo cada indice una linea (no tiene ni saltos de linea el fichero ¿?).

PD2: necesitas que PHP haya sido compilado con soporte para "mbstring" (http://es.php.net/manual/es/book.mbstring.php) que en Gentoo no se si esta por defecto. En FreeBSD, que es el que utilizo virtualizado para programar, no lo esta... pero creo que es una extension bastante comun (tampoco soy programador, simplemente hago mis pinitos).

----------

## i92guboj

 *Stolz wrote:*   

> 
> 
> Versión larga
> 
> Me ha tocado hacerme cargo de una web que hay que traducir. En un intento inicial de traducción el autor original encerró todas las frases susceptibles de ser traducidas de la forma que exige GNU Gettext, es decir así: _('Frase a traducir'). En principio las herramientas de gettext permiten extraer las cadenas del código que cumplan esa característica sin mayor problema. El problema es que el autor original no entendió muy bien la documentación de GNU gettext o se olvidó de leer la parte en la que se dice que el idioma original debe tener codificación ASCII y no se le ocurrió mejor idea que hacer la web original en portugués y UTF8.  Al intentar usar las herramientas de gettext fallan en cuanto encuentran un carácter especial (tilde,diéresis o similar) y el portugués está lleno de ellos. Para no realizar la sustitución/traducción a mano había pensado en automatizarlo con la línea de comandos, ya que cientos de frases re repiten una y otra vez en el código y mi nivel de portugués es mas bien ínfimo.
> ...

 

No he leído todo el hilo así que perdóname si no he entendido bien el problema. De todas formas, si todo lo que quieres es sustituir algunos caracteres determinados por cadenas, lo más sencillo es crear una especie de tabla de traducción. Dichos caracteres, en principio, no deberían aparecer en el html ni en ninguna otra parte.

Algo en la línea de:

```

#!/bin/bash

find . -name '*.php' | while read file

 sed -i 's/ç/cedilla_string/g' "$file"

 sed -i 's/á/a_string/g' "$file"

 sed -i 's/é/e_string/g' "$file"

 sed -i 's/í/i_string/g' "$file"

 sed -i 's/ó/o_string/g' "$file"

 sed -i 's/ú/u_string/g' "$file"

done

```

No lo he probado ni nada, es un boceto con la idea tan solo, no se si te será útil.

----------

