Publi

Encadenando comandos en C : ls | grep | wc

Es una práctica muy habitual y muy simple desde terminal, cuando queremos que la salida de un comando de la izquierda sea la entrada del comando de la derecha. Aunque puede ser que tal vez queramos crear un programa que ejecute justamente eso.

Para ilustrar esto vamos a ejecutar $ ls -R /mi/directorio/de/fotos | grep -i ‘jpg\|png’ | wc -l con esto, conseguiremos contar todas las fotos que hay en nuestro directorio de fotos (siempre que sean jpg o png). Empezamos analizando el comando que vamos a enviar:

  • ls -R /mi/directorio/de/fotos : listará todos los archivos a partir de /mi/directorio/de/fotos listando también los que encuentre en los subdirectorios (-R)
  • grep -i ‘jpg\|png’ : de la entrada que le pasemos, seleccionará, sin importar mayúsculas o minúsculas (-i) todas las líneas que contengan jpg o png. En realidad ponemos las ‘ ‘ para que bash no interprete lo que ponemos dentro, en C no nos harán falta.
  • wc -l : Cuenta las líneas que le pasemos a la entrada y saca el número por pantalla

La clave de todo esto reside en crear pipes por cada ejecución que vamos a hacer, y redirigir la entrada y la salida de dicho comando, que siempre será la entrada y la salida estándar a la pipe según nos convenga. Como además, ejecutando ls no termina todo, sino que luego querremos hacer algo con la salida, tendremos que crear un fork() que se encargue de ejecutar ls. Esto es así porque una vez que ejecutamos algo con la orden exec() o cualquiera de sus variantes, una vez que termina el proceso invocado terminará nuestro programa.

Como primer paso, vamos a redirigir ls:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <unistd.h>
#include <wait.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>

#define ERROR_CODE 1
#define BUFFER_SIZE 20

void error(char *msg)
{
  fprintf(stderr, "Error %s (errno: %d %s)\n", msg, errno, strerror(errno));
  exit(ERROR_CODE);
}

int main(int argc, char **argv)
{
  int pipels[2];        /* Las pipes se componen de dos descriptores, uno de entrada y otro de salida */
  int forkls;
  int lectura;
  char buffer[BUFFER_SIZE];

  if (pipe(pipels)==-1)
    error("No puedo crear la pipe");

  forkls=fork();
  if (forkls==-1)
    error("No puedo hacer fork");
  else if (forkls==0)
    {
      /* Para el proceso hijo... */
      close(pipels[0]);     /* cerramos la lectura de pipe */
      close(1);         /* cerramos la salida estandar */
      dup(pipels[1]);       /* redirigimos la salida estandar a la escritura en pipe */
      close(pipels[1]);     /* cerramos la escritura en pipe */

      execlp("ls", "ls", "-R", NULL);
    }
  /* Cerramos la escritura en pipe para el proceso padre */
  close(pipels[1]);
  /* Leemos de la pipe */
  while ((lectura=read(pipels[0], buffer, BUFFER_SIZE))>0)
    {
      buffer[lectura]='\0';
      printf ("ME HA LLEGADO: %s\n", buffer);
    }
  wait(NULL);           /* Esperamos que el proceso hijo muera */
  return 0;
}

¡La salida es muy fea! Se ve todo el rato ME HA LLEGADO: … eso es porque quería dejar constancia de que el que escribe en pantalla no es ls, sino el while() que tenemos debajo.

Si observamos, sólo hemos redirigido la salida estándar a una pipe y hemos leído de la pipe, y son un número considerable de líneas (unas 50), y además queda un código muy específico para la tarea, nada reutilizable. El objetivo es hacer algo más elegante, que nos sirva para el futuro, y que no incremente tanto el número de líneas (suponiendo que es proporcional, son 3 comandos, 150 líneas, y hay redirecciones de entrada/salida, por lo que nos sale alrededor de 170 líneas).

Vamos a crear una función que nos ejecute el exec y nos redirija la entrada y la salida, además, contamos con una tercera salida, la salida de error y en ocasiones es interesante redirigirla también. Por otro lado, la función queremos llamarla de forma intuitiva:

1
exec_call("comando", entrada, salida, error, parametro1, parametro2, NULL);

Es un estilo a execlp(), que podemos especificar cuantos argumentos queramos, siempre que el último sea NULL; pero en este caso, hacemos los close() y los dup() para redirigir la salida (podíamos utilizar dup2()/dup3() para ahorrarnos un close() pero bueno). Para poder introducir los parámetros que queramos a una función (sin límite), os remito a un antiguo post.

El código, sólo para ls quedaría así:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <unistd.h>
#include <wait.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>

#define ERROR_CODE 1
#define BUFFER_SIZE 20

void error(char *msg, char *msg2)
{
  fprintf(stderr, "Error %s%s (errno: %d %s)\n", msg, msg2, errno, strerror(errno));
  exit(ERROR_CODE);
}

void closedup(int des1, int des2, char* what)
{
  if (close(des1)==-1)
    error("closing std ", what);
  if (dup(des2)==-1)
    error("redirecting std ", what);
  if (close(des2)==-1)
    error("closing ", what);
}

int exec_call(char *command, int fdin, int fdout, int fderr,...)
{
  va_list arguments;
  char **argv;
  int argc=0;
  int i;

  if (fdin!=STDIN_FILENO)
    closedup(0, fdin, "input");

  if (fdout!=STDOUT_FILENO)
    closedup(1, fdout, "output");

  if (fderr!=STDERR_FILENO)
    closedup(2, fderr, "error output");

  va_start(arguments, fderr);
  while (va_arg(arguments, char*)!=NULL)
    argc++;
  va_end(arguments);

  argv=calloc(++argc, sizeof (char*));

  va_start(arguments, fderr);
  for (i=0; i<argc; i++)
    {
      argv[i]=va_arg(arguments, char*);
    }
  argv[i]=NULL;

  va_end(arguments);

  if (execvp(command, argv)==-1)
    error("execvp failed", "");
}

int main()
{
  char *photodir="/media/Fotos/";
  int pipels[2];
  int pipegrep[2];
  int ls;
  int grep;
  int lectura;
  char buffer[BUFFER_SIZE];

  if (pipe(pipels)==-1)
    error("creating pipe", "");
 
  ls=fork();
  if (ls==-1)
    error("forking ls", "");
  else if (ls==0)
    exec_call("ls", 0, pipels[1], 2, "ls", "-R", photodir, NULL);
  close(pipels[1]);

  while ((lectura=read(pipels[0], buffer, BUFFER_SIZE))>0)
    {
      buffer[lectura]='\0';
      printf ("ME HA LLEGADO: %s\n", buffer);
    }
  wait(NULL);           /* Esperamos que el proceso hijo muera */
  return 0;
}

Son prácticamente 100 líneas, pero si os fijáis el main() está mucho más limpio, y si lo pensáis bien, será muy fácil añadir más comandos a la cadena, y redirigir entradas y salidas. En este caso de ls(), la entrada no la redirigimos, 0 es la entrada estándar (también se puede usar la constante STDIN_FILENO), pero la salida no es 1 (salida estándar o STDOUT_FILENO), sino es la escritura de la pipe pipels (una pipe será una pareja de descriptores, uno de lectura y otro de escritura).

El listado ahora, se hace de photodir que contiene el directorio donde tenemos las fotos.

Ahora bien, si queremos escribir el comando que os puse al principio, sólo habrá que cambiar el main() por:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int main()
{
  char *photodir="/media/7514-F917/";
  int pipels[2];
  int pipegrep[2];
  int ls;
  int grep;
  if (pipe(pipels)==-1)
    error("creating pipe", "");
 
  ls=fork();
  if (ls==-1)
    error("forking ls", "");
  else if (ls==0)
    exec_call("ls", 0, pipels[1], 2, "ls", "-R", photodir, NULL);
  close(pipels[1]);
 
  if (pipe(pipegrep)==-1)
    error("creating pipe grep", "");
  grep=fork();
  if (grep==-1)
    error("forking grep", "");
  else if (grep==0)
    exec_call("grep", pipels[0], pipegrep[1], 2, "grep", "-i", "jpg\\|png", NULL);

  close(pipegrep[1]);
  exec_call("wc", pipegrep[0], 1, 2, "wc", "-l", NULL);
}

Al final hemos creado dos fork() y dos pipes() el primero será para ls, que leerá de la entrada estándar (no vale para nada, porque ls no necesita ninguna entrada) y escribirá en pipels, luego creamos otro fork+pipe para grep, éste tomará su entrada de pipels y su salida la pasará por pipegrep. Como queremos la salida de wc, no tenemos que crear más pipes (aunque seria lo suyo), además, la salida será la salida estándar (y su entrada pipegrep).

Vayamos un paso más allá, podemos crear también el proceso hijo desde nuestra función, y así limpiamos aún más el main(), además, para terminar el ejemplo, ya que wc nos devuelve un número, transformaremos ese número (que nos llega como cadena de caracteres en un entero):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <unistd.h>
#include <wait.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>

#define ERROR_CODE 1
#define BUFFER_SIZE 20

void error(char *msg, char *msg2)
{
  fprintf(stderr, "Error %s%s (errno: %d %s)\n", msg, msg2, errno, strerror(errno));
  exit(ERROR_CODE);
}

void closedup(int des1, int des2, char* what)
{
  if (close(des1)==-1)
    error("closing std ", what);
  if (dup(des2)==-1)
    error("redirecting std ", what);
  if (close(des2)==-1)
    error("closing ", what);
}

int exec_call2(char *command, int fdin, int fdout, int fderr,...)
{
  va_list arguments;
  char **argv;
  int argc=0;
  int i;
  int hijo=fork();
  if (hijo==-1)
    error("fork", "");
  else if (hijo>0)        /* Si no somos el proceso hijo,  */
    return 0;             /* salimos de la función */
 
  if (fdin!=STDIN_FILENO)
    closedup(0, fdin, "input");

  if (fdout!=STDOUT_FILENO)
    closedup(1, fdout, "output");

  if (fderr!=STDERR_FILENO)
    closedup(2, fderr, "error output");

  va_start(arguments, fderr);
  while (va_arg(arguments, char*)!=NULL)
    argc++;
  va_end(arguments);

  argv=calloc(++argc, sizeof (char*));

  va_start(arguments, fderr);
  for (i=0; i<argc; i++)
    {
      argv[i]=va_arg(arguments, char*);
    }
  argv[i]=NULL;

  va_end(arguments);

  if (execvp(command, argv)==-1)
    error("execvp failed", "");

  return 0;           /* Nunca llegaremos aquí */
}


int main()
{
  char *photodir="/media/7514-F917/";
  int pipels[2];
  int pipegrep[2];
  int pipewc[2];
  int ls;
  int grep;
  int wc;
  int lectura;
  char *buffer[BUFFER_SIZE];

  if (pipe(pipels)==-1)
    error("creating pipe", "");
 
  exec_call2("ls", 0, pipels[1], 2, "ls", "-R", photodir, NULL);
  close(pipels[1]);

  if (pipe(pipegrep)==-1)
    error("creating pipe", "");
 
  exec_call2("grep", pipels[0], pipegrep[1], 2, "grep", "-i", "jpg\\|png", NULL);
  close(pipegrep[1]);

  if (pipe(pipewc)==-1)
    error("creating pipe", "");

  exec_call2("wc", pipegrep[0], pipewc[1], 2, "wc", "-l", NULL);
  close(pipewc[1]);

  wait(NULL);
  wait(NULL);
  wait(NULL);

  lectura=read(pipewc[0], buffer, BUFFER_SIZE);
  if (lectura<0)
    error("reading pipe", "");
  buffer[lectura]='\0';

  printf("El resultado es: %d\n", atoi((const char*)buffer));

  return 0;
}

Ahora es mucho más fácil encadenar cuantos procesos sean necesarios para cumplir nuestros objetivos. Sólo quedan dos detalles por comentar, en este último código he incluído tres líneas idénticas:

1
wait(NULL);

Es para finalizar correctamente los procesos hijo, algo así como reclamar los restos de un proceso que ha muerto.

Además, cuando hacemos la lectura (esta vez no he hecho un while, porque sólo queremos leer un número), justo después del read() hago un buffer[lectura]=’\0′; eso es porque read() devuelve el número de caracteres leídos, y deseamos poner un carácter terminador al final de la cadena leída, para que cuando atoi() (o quien venga) la lea no de ningún fallo.

También podría interesarte...

Only 1 comment left Ir a comentario

  1. Pingback: Bitacoras.com /

Leave a Reply