Programando para Linux

De Jose Castillo Aliaga
Ir a la navegación Ir a la búsqueda

Este artículo trata de hacer un programa siguiendo los estándares de los programas para GNU/Linux. La mayoría de programas de terminal de Linux cumplen en mayor o menor medida unas pautas para facilitar su uso y aprendizaje. Estas están relacionadas con la filosofía UNIX. Que, resumiendo, se trata de hacer programas que hagan una cosa y la hagan bien, que puedan trabajar juntos y que manejen texto, ya que es la interfaz universal. De esta manera, difícilmente, un programa que solo tenga interfaz gráfica, que haga de todo un poco pero nada demasiado bien o que tenga una formato propietario para guardar sus datos, podrá cumplir los ideales de esta filosofía.

Dentro de esta manera de pensar, otros autores han concretado más las reglas para hacer un buen programa para Unix. A continuación voy a resumir las que me parecen más relevantes y intentaré cumplir en este artículo:

  • Modularidad: Todo programa mínimamente complejo ha de estar formado por módulos que se comunican con unas interfaces bien definidas para poder mejorar los módulos por separado.
  • Claridad: Se debe hacer código legible por el programador y otros. Como se trata de hacer software libre esta regla es bastante importante.
  • Composición: Nuestro programa ha de poder comunicarse fácilmente con los demás para poder hacer comandos más complejos. Por ejemplo, los comandos cat, cut, tr, grep, sed... se pueden poner sin problemas entre | para ir filtrando un texto.
  • Simplicidad: Un programa potente no es necesariamente un programa con miles de opciones y parámetros. Algunos comandos pueden descorazonar al usuario al entra en su manual. Si el programa ha de ser complejo, se debería facilitar un uso básico fácil y una curva de aprendizaje asequible.
  • Robustez: El programa ha de ser capaz de hacer bien su trabajo en cualquier condición.
  • Pocas sorpresas: El programa tendrá una interfaz similar a todos los demás. Por ejemplo, el signo + ha de significar siempre sumar o añadir algo.
  • Silencio: A menos que se le indique, el programa solo sacará los datos pedidos. De esta manera, se pueden enlazar los programas.
  • Fallos ruidosos: Si algo falla, el usuario ha de ser consciente de todos los detalles.
  • Economía: En términos de ciclos de reloj y espacio en memoria.
  • Utilizar ficheros de texto plano: Puede que no sea lo más eficiente. Pero los podrá entender cualquiera y los podrá utilizar cualquier programa. Si es necesario, se pueden comprimir después.
  • Preferencia por la terminal: Todo lo que se puede hacer de forma gráfica se ha de poder hacer por terminal.

A lo largo del artículo nos centraremos en cómo hacer todo esto de manera técnica.

El programa

Hay programas para casi todo en la terminal y no voy a poder hacer algo totalmente nuevo. La función del programa es hacer un traductor de texto plano a braille. Para ello utlitzaremos los caracteres que posee Unicode para braille.

Las funciones del comando braille son las siguientes:

  • Actuar como un filtro similar al tr. Tendrá entrada y salida estándar.
  • Traducir un fichero de texto diréctamente y guardar en uno.
  • Traducir al braille español (de momento) pero permitir elegir el idioma si se implementan más.

De esta manera, ese comando:

$ echo "Hola Mundo" | braille
⡓⠕⠇⠁⠀⡍⠥⠝⠙⠕

Retorna el texto que se introduce por la stdin y lo muestra por pantalla.

Este comando puede ser una buena base porque trata de manera bastante compleja el tratamiento de la entrada/salida y también tiene interpretación de argumentos y opciones.

Implementación

El programa va a ser programador en C.

Interacción con el entorno

C y C++ proporcionan unas interfaces con el sistema Linux para introducir o extraer datos en los programas. Las más importantes son los argumentos, que se pasan al programa en el momento de su ejecución y la entrada y salida estándar para comunicarse con el proceso cuando está ya en marcha.

Los argumentos se pasan con:

  • argc: Variable int que es inicializada en el número de argumentos que se pasan al programa.
  • argv: Array de char* (Punteros a cadenas de caracteres) que contiene cada uno de los argumentos que se han pasado.

Estos argumentos pueden ser opciones o otros argumentos. Las opciones, por convención en Linux, van precedidas por el signo - y modifican aspectos del comportamiento del programa. Estas pueden ser:

  • Cortas: Van precedidas de un solo - y tienen un sola letra. -s -l -A -X...
  • Largas: Van precedidas de dos -- y tiene la palabra completa. Estas son más fáciles de recordar y de entender cuando se ve el comando que ha hecho otro.

Los programas típicos de Unix solían tener opciones cortas. A partir de GNU, también se popularizaron las opciones largas.

Muchas opciones cortas tienen un equivalente en opciones largas. Esto permite alternar entre velocidad o legibilidad.

Leer todas las opciones puede ser un trabajo tedioso. Algunas necesitan un argumento, pueden ir en cualquier orden, pueden ser largas o cortas, pueden tener diferentes espacios... Se puede hacer a mano: un for que recorra argv y que contemple todas estas posibilidades. Sin embargo, hay una librería que simplifica todo esto y evita errores: getopt_long

getopt_long

Se trata de una función que proporciona C y C++ para leer tanto opciones largas como cortas. Para usarlo hay que incluir la biblioteca:

#include <getopt.h>

En el caso de nuestra aplicación, aceptará estas opciones:

Forma corta Forma Larga Utilidad
-f --fileinput Fichero de entrada en caso de que no se use la entrada estándar.
-o --outputfile Fichero de salida en caso de que use la salida estándar.
-h --help Ayuda

Además el programa acepta no tener argumentos.

getopt_long necesita dos estructuras de datos. Una para las opciones cortas y otra para las largas.

La primera es un string con todos los caracteres cortos seguidos. Aquellos que necesitan un argumento extra (la ruta del fichero), tienen : detrás de la letra:

const char* const short_options = "ho:f:";

La segunda estructura es un array de struct con 4 campos: El primero es el string del nombre largo, el segundo es 0 si no necesita argumentos y 1 si los necesita, el tercero es NULL y el cuarto es el equivalente corto.

 const struct option long_options[] = {
  { "help", 0,NULL, 'h'},
  { "output", 1, NULL, 'o'},
  { "fileinput", 1, NULL, 'f'},
  { NULL, 0, NULL, 0}
 };

El último elemento está todo a 0.

Se invoca getopt_long pasando el argc, el argv y las dos estructuras:

next_option = getopt_long (argc,argv,short_options,long_options,NULL);
  • Cada vez que se invoca esta función, retorna una sola opción. Por tanto, se suele usar dentro de un bucle para ir recogiendo todos.
  • Si se recoge una opción que no está contemplada, imprime un mensaje de error y recoge el caracter ?.
  • Si se requiere un argumento para la opción, este se recoge con la variable optarg.

De esta manera, el código para recoger las opciones y argumentos queda así:

 /* El nombre del fichero de salida y de entrada*/
 const char* output_filename = NULL;
 const char* input_filename = NULL;
 program_name = argv[0];

 do {
  next_option = getopt_long (argc,argv,short_options,long_options,NULL);
  switch (next_option)
  {
  case 'h': /* -h or --help */
     print_usage (stdout, 0);
  case 'o':
     output_filename = optarg;
     break;
  case 'f':
     input_filename = optarg;
     break;
  case '?':
     print_usage (stderr, 1);
  case -1:
     break;
  default:
     abort (); 
  }
 }
 while (next_option != -1);

Esta es la función print_usage() usada tanto para escribir por la stdout el manual como por la stderr en caso de fallo:

void print_usage (FILE* stream, int exit_code)
{
 fprintf(stream, "Usage: %s options [ -f inputfile ...]\n", program_name);
 fprintf(stream,
        " -h --help             Muestra informacion de uso.\n"
        " -o --output filename  Guarda la salida en un fichero.\n"
        " -f --fileinput filename       Permite seleccionar un fichero de entrada. \n");
 exit (exit_code);
}

Veamos cómo funciona esta función:

Entrada y Salida estándar

La biblioteca stdio.h proporciona la entrada y salida estándar. (stdin y stdout). Estas se usan con funciones como scanf o printf. La tradición de Unix dice que hay que usar la entrada y salida estándar para poder enlazar varios programas con | (pipes o tuberías) o para redireccionar la salida con > <. También está stderr para la salida de errores, que suele ser por pantalla.

Podemos modificar la salida estándar o la entrada estándar para escribir o leer a ficheros y no a la pantalla. Por ejemplo, si queremos escribir algo por la salida de error:

fprintf (stderr, ("Error ...."));

Hay que mencionar que stdout se comporta como un buffer. Es decir, no escribe en consola lo que ese escribe hasta que el buffer está lleno, el programa acaba o se cierra stdout. Se puede vaciar el buffer con la orden:

fflush (stdout);

stderr no tiene buffer. Observa el curioso comportamiento de este programa:

 #include <stdio.h>
 int main(){
  while (1){
   printf(".");
   fprintf(stderr, ":");
   usleep (10000);
  }
  }

En nuestro caso, la entrada la podríamos hacer con getchar(). Esta función recoge un carácter por la entrada estándar. Sin embargo, a veces necesitaremos recoger los caracteres de un fichero. Así que podemos usar getc() indicando cual es el fichero. Este puede ser stdin o input_filename. Los dos son estructuras tipo FILE. Se trata de un tipo que cuenta con información para controlar un stream como la posición de fichero o indicador de fin.

Códigos de salida

El código de salida es un entero que indica si ha acabado con éxito o no el programa. El 0 indica una ejecución exitosa y números mayores pueden indicar distintos tipos de errores.

En bash, se puede obtener este código con la variable $?.

En C, la instrucción return 0; del main es la que indica que ha finalizado con éxito. También se puede indicar con exit(); en otras funciones.

UTF-8

Como este programa va a usar caracteres UNICODE para imprimir las letras en UNICODE, es necesario usar funciones de entrada/salida adaptadas a caracteres de más de 8 bits.

Cómo usarlos se puede ver en este artículo.

Variables de entorno

Los programas en Linux pueden acceder a las variables de entorno del sistema. Tanto el nombre como el valor de las variables son Strings.

Para hacerlo se necesita la biblioteca <stdlib.h> para usar la función getenv o setenv

Ficheros temporales

Se pueden usar ficheros temporales en el directorio /tmp teniendo en cuenta algunas precauciones:

  • Pueden haber varias instancias del mismo programa y cada una debe tener un fichero de nombre distinto.
  • Los permisos del fichero temporal han de estar bien configurados, ya que /tmp es accesible por todos.
  • El nombre de los ficheros temporales han de ser difícilmente predecible para evitar ataques.

GNU/Linux proporciona las funciones mkstemp y tmpfile.

Resultado

Este es todo el código resultante:

#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include<wchar.h>
#include <locale.h>

/* Nombre del programa */
const char* program_name;

/* Imprime informacion de uso al STREAM (stdin o stderr) y sale EXIT_CODE
No tiene return */

void print_usage (FILE* stream, int exit_code)
{
 fprintf(stream, "Usage: %s options [ -f inputfile ...]\n", program_name);
 fprintf(stream,
	" -h --help		Muestra informacion de uso.\n"
	" -o --output filename	Guarda la salida en un fichero.\n"
	" -f --fileinput filename	Permite seleccionar un fichero de entrada. \n");
 exit (exit_code);

}

void print_translation(FILE* stream, FILE* instream)
{

  setlocale(LC_ALL, "");

  wchar_t c; /* the character read*/
  int i=0;
 // Este while va recorriendo todas las letras que toma de la entrada estándar. Las pasa a minúsculas y busca su equivalente en Unicode.
 // Unicode no tiene las letras de forma secuencial, por lo que se hace necesario usar un 'case'
  while ((c = tolower(getwc(instream))) != EOF)
  {   
 //fputwc(c,stream);
   i++;
    switch (c)
     {
      case 'a': fputwc((0x2801),stream); break;
      case 'b': fputwc((0x2803),stream); break;
      case 'c': fputwc((0x2809),stream); break;
      case 'd': fputwc((0x2819),stream); break;
      case 'e': fputwc((0x2811),stream); break;
      case 'f': fputwc((0x280b),stream); break;
      case 'g': fputwc((0x281b),stream); break;
      case 'h': fputwc((0x2813),stream); break;
      case 'i': fputwc((0x280a),stream); break;
      case 'j': fputwc((0x281a),stream); break;
      case 'k': fputwc((0x2805),stream); break;
      case 'l': fputwc((0x2807),stream); break;
      case 'm': fputwc((0x280d),stream); break;
      case 'n': fputwc((0x281d),stream); break;
      case L'ñ': fputwc((0x283b),stream); break;
      case 'o': fputwc((0x2815),stream); break;
      case 'p': fputwc((0x280f),stream); break;
      case 'q': fputwc((0x280d),stream); break;
      case 'r': fputwc((0x2817),stream); break;
      case 's': fputwc((0x280e),stream); break;
      case 't': fputwc((0x281e),stream); break;
      case 'u': fputwc((0x2825),stream); break;
      case 'v': fputwc((0x2827),stream); break;
      case 'w': fputwc((0x283a),stream); break;
      case 'x': fputwc((0x282d),stream); break;
      case 'y': fputwc((0x283d),stream); break;
      case 'z': fputwc((0x2835),stream); break;
      case L'á': fputwc((0x2837),stream); break;
      case L'é': fputwc((0x282e),stream); break;
      case L'í': fputwc((0x280c),stream); break;
      case L'ó': fputwc((0x282c),stream); break;
      case L'ú': fputwc((0x283e),stream); break;
      
      case '1': fputwc((0x2801),stream); break;
      case '2': fputwc((0x2803),stream); break;
      case '3': fputwc((0x2809),stream); break;
      case '4': fputwc((0x2819),stream); break;
      case '5': fputwc((0x2811),stream); break;
      case '6': fputwc((0x280b),stream); break;
      case '7': fputwc((0x281b),stream); break;
      case '8': fputwc((0x2813),stream); break;
      case '9': fputwc((0x280a),stream); break;
      case '0': fputwc((0x281a),stream); break;

	//default
	default: fputwc(c,stream); break;
      }
  }
}


/* argc contiene el numero de argumentos y argv un array de punteros a ellos*/

int main (int argc, char* argv[])
{
 int next_option;

 /* Un string que contiene las opciones validas */
 const char* const short_options = "ho:f:";
 /* Un array con las opciones largas validas*/
 const struct option long_options[] = {
  { "help", 0,NULL, 'h'},
  { "output", 1, NULL, 'o'},
  { "fileinput", 1, NULL, 'f'},
  { NULL, 0, NULL, 0}
 };

/* El nombre del fichero de salida y de entrada*/
 const char* output_filename = NULL;
 const char* input_filename = NULL;
 program_name = argv[0];

 do {
  next_option = getopt_long (argc,argv,short_options,long_options,NULL);
  switch (next_option)
  {
  case 'h': /* -h or --help */
     print_usage (stdout, 0);
  case 'o':
     output_filename = optarg;
     break;
  case 'f':
     input_filename = optarg;
     break;
  case '?':
     print_usage (stderr, 1);
  case -1:
     break;
  default:
     abort (); 
  }

 }
 while (next_option != -1);

 FILE* input_file = stdin;
 FILE* output_file = stdout;

 if(input_filename){
 	input_file=fopen(input_filename,"r");
 }

 if(output_filename){
	output_file=fopen(output_filename,"w");
 }


 print_translation(output_file,input_file); 
 return 0;
}

MISC

Colores en la terminal

Es posible usar caracteres de escape en C para representar colores en las terminales modernas de Linux. Por ejemplo:

#include <stdio.h>

#define ANSI_COLOR_RED     "\x1b[31m"
#define ANSI_COLOR_GREEN   "\x1b[32m"
#define ANSI_COLOR_YELLOW  "\x1b[33m"
#define ANSI_COLOR_BLUE    "\x1b[34m"
#define ANSI_COLOR_MAGENTA "\x1b[35m"
#define ANSI_COLOR_CYAN    "\x1b[36m"
#define ANSI_COLOR_RESET   "\x1b[0m"

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

  printf(ANSI_COLOR_RED     "This text is RED!"     ANSI_COLOR_RESET "\n");
  printf(ANSI_COLOR_GREEN   "This text is GREEN!"   ANSI_COLOR_RESET "\n");
  printf(ANSI_COLOR_YELLOW  "This text is YELLOW!"  ANSI_COLOR_RESET "\n");
  printf(ANSI_COLOR_BLUE    "This text is BLUE!"    ANSI_COLOR_RESET "\n");
  printf(ANSI_COLOR_MAGENTA "This text is MAGENTA!" ANSI_COLOR_RESET "\n");
  printf(ANSI_COLOR_CYAN    "This text is CYAN!"    ANSI_COLOR_RESET "\n");

  return 0;
}

TODO

Comunicación entre procesos, tuberías...

fork()

Enlaces