h1

Cómo leer líneas de texto del teclado en C

3 \03UTC marzo \03UTC 2008

Estuve hablando con knithx el otro día sobre esto. Parece una tarea trivial, y de hecho lo es. Pero el problema es que el 95% de los casos, es algo que explican mal. Y lo tengo comprobado yo mismo multitud de veces.

El primer problema reside en el concepto de “teclado”. En C, el teclado es un fichero, designado por stdin. Y como todos los ficheros en C, stdin tiene una declaración del tipo:

FILE *stdin;

Declarado en stdio.h. Que quede bien claro, desde nuestro programa no tendremos un teclado. Tendremos un archivo “un poco especial” que todo lo que leemos de él se corresponde milagrosamente con lo que introducimos por el teclado, nada más. A esto habría que añadir que en sistemas Linux, stdin se corresponde con el descriptor de fichero 0, y que puede ser alterado externamente por un programa, etcétera, etcétera. No voy a centrarme en eso, intentaré explicar los problemas que tiene todo esto tan sólo con la interfaz de C.

Otra cosa que debemos tener en cuenta con stdin es que es un fichero tamponado (buffered para los yankees). Esto es una cosa que la gente suele pasar por alto, y es muy importante. Cuando decimos que un fichero es tamponado, quiere decir que hay una memoria interna, memoria tampón o buffer la cual se va llenando poco a poco, hasta cierto momento que toca vaciarla. En el caso de lectura de ficheros, esto se traduce en que las funciones de entrada (fgets, gets, scanf, fscanf, fgetc y sí, getchar también) sólamente volverán cuando se cumpla una de estas tres condiciones:

  • El buffer se llene. Por suerte, es algo bastante amplio, y en la mayoría de ocasiones tendremos que leer datos de un tamaño considerablemente menor. El tamaño por defecto de este buffer viene dado por la constante BUFSIZ definida en stdio.h, y en el caso de mi sistema, es de 8 KiB. De sobra.
  • Se alcance un fin de línea. Este va a ser el caso más corriente. El usuario escribe un dato y pulsa el ENTER pada enviarlo o confirmarlo.
  • Se alcance un fin de fichero. No es común, pero el usuario puede provocarlo de varias maneras. En sistemas basados en Linux, la manera más común es introduciendo un ^D (es decir, pulsando Control+D). Pero como veremos más adelante, esto puede no ser así siempre.

Y por último, la mayor parte de los errores vienen por no usar (o no saber usar adecuadamente) las funciones de entrada que nos proporciona stdio.h. Este problema se arregla teniendo bien claro que para leer, sólo se nos permitirá usar esta función:

char *fgets (char *s, int tam, FILE *flujo);

Estas están prohibidas:

char *gets(char *s);
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int vscanf(const char *format, va_list ap);

Y hago mención especial a scanf, quizá la más puñetera de las funciones de entrada. Lo es, porque normalmente lee un pedazo del buffer, dejando cosas por leer, y cuando queremos hacer una segunda lectura, en vez de leer del teclado lee lo que quedaba en el buffer, trayendo consigo errores aparentemente indepurables.

Ya tenemos todo. Y es que usaremos fgets para leer líneas de texto y nada más, así que explicaré a fondo el uso de esta función (que debería ser la única).

El uso de fgets para leer líneas es bastante automático, sencillamente:

char cadena [TAMANHO];
fgets (cadena, TAMANHO, stdin);

Donde cadena puede ser un array de caracteres (como es el caso) o un puntero a una cadena, inicializada dinámicamente con malloc, por ejemplo.

Nótese que el tamaño pasado a fgets coincide con el del número de bytes de la propia cadena (nunca debe rebasar el tamaño de la cadena). Digo “número de bytes” y no “tamaño de la cadena” porque normalmente no (de hecho, nunca) es lo mismo. Cuando llamamos a fgets, la función espera un pedazo de memoria de un tamaño determinado donde escribir.

Es decir, fgets no escribirá más bytes en cadena que los especificados por TAMANHO, en el cual se incluye el caracter nulo (‘\0′). Este caracter aparecerá siempre que llamemos a fgets, garantizando la legibilidad de la cadena leída.

Ya tenemos el “como hacer las cosas”. Ahora nos queda interpretar los datos recibidos por estas funciones:

¿Qué comportamiento es de esperar cuando un usuario introduce una línea y pulsa el ENTER?

Tenemos el código siguiente:

#include <stdio.h>
 
int main ()
{
  char cadena [100];
  printf ("Introduzca una cadena: ");
  fgets (cadena, 100, stdin);
  printf ("La cadena leída es: "%s"n", cadena);
  return 0;
}

Lo que hace es básico. Parece pedir una cadena y la mostrarla por pantalla entrecomillada. ¿Seguro? Probemos:

$ ./fgets_prueba
Introduzca una cadena: Hola mundo.
La cadena leída es: "Hola mundo.
"

O-oh. Comportamiento inesperado. ¿O no?

Lo que fgets ha hecho fue, efectivamente leer la cadena y el salto de línea (el ENTER) que hemos pulsado. El ENTER no es ninguna tecla mágica especial en la consola, es un caracter más que en el caso de ficheros tamponados provocan que se copie el contenido del buffer en la memoria y se devuelva el control. Pero el caracter del ENTER (el salto de línea, representado por ‘\n’) también se lee. Lo que quiere decir que lo que hemos leído del teclado no es:

"Hola mundo."

Si no, más bien:

"Hola mundo.\n"

Ese caracter \n en la mayoría de ocasiones nos es un estorbo. No es problema, podemos codificar una pequeña función que mediante la función strchr (cuyo prototipo se encuentra en string.h) nos lo busquen y lo eliminen:

void limpiar (char *cadena)
{
  char *p;
  p = strchr (cadena, '\n');
  if (p)
    *p = '\0';
}

Lo que viene a hacer es buscar ese salto de línea y convertirlo en un caracter de fin de cadena. Es decir, hacemos que la cadena acabe donde acaba la línea. Es importante darse cuenta de que con ese if (p) hago una comprobación, detecto si el caracter de fin de línea se ha encontrado o no (que puede no aparecer por diversas razones, como que el buffer se haya quedado al completo o que se haya alcanzado un fin de fichero). Es decir, pasamos de tener:

Con salto.

A tener:

Sin salto.

Que a efectos prácticos, es una cadena que acaba en su última letra (un punto en este caso).

Este es sin duda el caso más usual, pero…

¿Qué comportamiento es de esperar cuando un usuario introduce un fín de fichero?
Supongamos el mismo código, sin la función de limpieza. Para hacer este experimento tenemos que asegurarnos de que en nuestra consola, el valor de fin de fichero (EOF) es efectivamente, Control+D. En la mayor parte de las veces no es necesario, pero para asegurarnos, ejecutamos:

$ stty eof ^D

Ahora ya podemos hacer el experimento. Escribamos una línea, pero pulsemos Control+D en vez de ENTER:

$ ./fgets_prueba
Introduzca una cadena: Hola mundo.█

Y el cursor se queda al final del punto, esperando a que escribamos más. ¿Por qué?

Pues nos resulta que fgets es un poco terco respecto del fin de fichero. Cuando pulsamos Control+D efectivamente, recibe internamente el fin de fichero, pero para él eso quiere decir otra cosa. Sigamos escribiendo después del punto y pulsemos ENTER al final como la otra vez:

$ ./fgets_prueba
Introduzca una cadena: Hola mundo. De colegueo y tal.
La cadena leída es: "Hola mundo. De colegueo y tal.
"

Ha ignorado completamente nuestro Control+D. Probemos ahora a escribir el mismo “Hola mundo.”, pero pulsando Control+D dos veces:

$ ./fgets_prueba
Introduzca una cadena: Hola mundo.La cadena leída es: "Hola mundo."

El comportamiento ha cambiado. Cuando recibe dos fin de fichero seguidos, directamente devuelve el control con los datos que se han leído. ¿Por qué?

Esto tiene que ver con la finalidad que se le ha dado a esta función. Aquí la hemos usado para leer líneas del teclado, pero es que donde he puesto stdin pude haber puesto cualquier otro FILE* válido, el cual puede referirse a un archivo de texto “físico”, y no a un dispositivo de entrada. La diferencia esencial radica en que mientras en un fichero hay un sólo fin de fichero, y que más allá de ese fin de fichero sigue estando el fin de fichero, en los terminales de texto no. El usuario puede inducir un fin de fichero en cualquier instante, pero después del mismo se pueden seguir introduciendo datos.

De esto se deriva el comportamiento de fgets. Si después de un fin de fichero se encuentra otro fin de fichero, fgets deduce que estamos leyendo un fichero físico en un disco, por lo que no se va a molestar en intentar leer más y devuelve el control con todo lo que se ha leído hasta el final. Sin embargo, si hay un fin de fichero, pero luego sigue habiendo más datos, sencillamente lo ignora, como si aquí nada hubiese pasado.

Por eso mismo inducimos el primer comportamiento pulsando dos veces seguidas el Control+D, haciendo creer a fgets que es un EOF de verdad. Nótese que lo que hemos hecho es equivalente a:

$ echo Hola mundo. | ./fgets_prueba

En este caso, lo que internamente hace nuestro intérprete de comandos (bash en mi caso) es sustituir la entrada de teclado de ./fgets_prueba por la salida de texto del comando echo. Este comando escribe “Hola mundo.”, texto que es enviado a nuestro programa y leído por fgets. Esta operación es realizada por una estructura especial denominada tubería o FIFO de la que no voy a hablar aquí, pero de la que hay amplísima documentación. El caso es que si de esta “tubería” cortamos la salida por un extremo, entonces no hay nada más que leer de ella, por lo que las sucesivas lecturas provocarán para fgets un fin de fichero, por lo que dejará de leer.

Y por último:

¿Cómo se debería comportar fgets cuando no lee nada?
Aquí tenemos que tener en cuenta que esto sólo puede ocurrir (en las condiciones más normales a las que nos vamos a enfrentar) si fgets recibe un fin de fichero antes de leer ningún caracter. Entonces fgets devuelve el contro al primer EOF que lee, y haciendo que la cadena leída tenga longitud cero.

Esta situación la podemos detectar de dos formas distintas, una es comprobando que la cadena leída tiene longitud cero (ya que si pulsásemos ENTER, lo que habríamos leído sería un caracter -el salto de línea-):

fgets (cadena, TAMANHO, stdin);
if (strlen (cadena) == 0)
{
  /* Acciones a realizar en este caso */
}

Y la otra es comprobando que el valor devuelto por fgets sea NULL. Para ello necesitamos incluir stdlib.h en nuestro código. La línea resultante sería:

if (fgets (cadena, TAMANHO, stdin) == NULL)
{
  /* Acciones a realizar en este caso */
}

Lo cual es mucho más inmediato y de hecho es lo que más se usa. Desde aquí, recomiendo usar este, pues se usa directamente la definición del comportamiento de fgets y en la documentación no se dice nada de lo que deja en cadena.

Conociendo esto, se puede llegar a uno de los usos más corriente de fgets, que es incluirlo en un bucle while:

while (fgets (cadena, TAMANHO, stdin))
{
  /* Cosas mientras leo líneas */
}

Esta es una estructura muy recurrente en programación de aplicaciones que accedan al teclado, y que conviene tenerla bien aprendida. Lo que hace es, básicamente, leer línea a línea, mientras haya líneas por leer. Es útil cuando, por ejemplo, lo que el programa espera en su entrada es un texto compuesto por varias líneas, o incluso un fichero de texto entero. La mayor parte de los comandos de filtro (grep, head, tail, tr, rev, sed…) se basan en mecanismos parecidos a este.

En resumen, la clave para saber leer del teclado sin sospresas desagradables consiste básicamente en leer líneas con fgets. El resto de formas tienen algún tipo de trampa, aunque hay otra vía (ir llendo caracter a caracter, quizá hable de esto otro día), y además hay que buscar formas de interpretar y transformar el texto leído (cosa de la que también debería explicar otro día diferente). Lo principal en este lenguaje es la experiencia personal, someter a fgets a las situaciones más bizarras y evaluar su comportamiento en la práctica.

Saludos

About these ads

30 comentarios

  1. Gracias por el manual, aunque no coincido mucho en una premisa.

    Está claro que scanf no siempre es muy útil, ya que no captura varias palabras separadas mediante espacios ( ‘ ‘), pero es que hay programas que no hace falta más. Típico caso, supongamos que hacemos un superprograma ultracomplejo como una calculadora (qué original), nosotros queremos introducir datos, para qué matarnos con fgets pudiendo usar scanf, con fgets tendriamos que hacer una soberana chapuza:
    declarar una matriz de tipo carácter, leer datos y luego hacer un casting a otra variable (pensando que somos limpios sin mostrar un printf directamente de las operaciones, ya que sería conveniente tener cierto control sobre operaciones como dividir por 0 y demás).
    Fíjate, no sale a cuenta, eso solo lo hacen los currantes, yo con un simple:
    //cabeceras
    void main(void) {
    int num;
    scanf(“%i”,&num);
    Y me quedo tan pancho.
    Un saludo y gracias por el minimanual.


  2. Magistral explicación, gracias :D


  3. Manda carallo…yo siempre use scanf, no tenia ni idea de eso de fgets.

    Por cierto, no entendi muy bien la diferencia entre scanf y fgets, pero si solo es por el problema de que lea tambien el ENTER o el control D(que no se lo que es)como un caracter estoy segura de que habia otra funcion para evitarlo, tendre que volver a mirar en mis libros.


  4. Usar fgets de esa forma puede llevar a errores.

    Si equivocas el tamaño que le pasas como 2do. argumento, a un error de sobreescritura de memoria y si escribes más caracteres de los que indicas leer con ese mismo argumento, quedan en el buffer tal como pasa con scanf y serán leídos en una siguiente lectura en forma inesperada.


  5. A ignorante:

    Obviamente, hemos de suponer que el programador no es tan cafre de ponerle un tamaño mayor que el del propio buffer.

    Y el otro problema… pf, claro. Si tenemos eso en cuenta, la solución puede volverse harto engorrosa. No sé, para la mayoría de problemas que se plantean, hay un margen relativamente amplio donde scanf es inútil y nunca superaremos las prestaciones fgets.

    ¡Cuánta gente quiso leer una frase corta del teclado, y el scanf con %s cascó en el primer espacio! Al menos, para programas de poca monta, el sistema funciona.

    Saludos


  6. Genial!


  7. Que te parece la solucion
    scanf(“%[^\n]“, frase);
    para leer con espacios incluidos toda la entrada?

    Yo lo veo tambien una solucion optima.


  8. A Javi:

    Eh, pues ese no me lo sabía, me lo guardo :D

    De todas formas, con fgets tengo la suerte de que puedo poner un tope. Aunque seguramente entre los infinitos formatos de scanf haya alguna forma de limitarlo, a saber.

    ¿En qué parte del manual sale eso? Tengo que mirarlo con más detenimiento.


  9. Holas,

    Y en caso de querer un buffer, o sea, que lea más de una línea de texto????

    Saludos


  10. Pues supongo que con ir leyendo varias líneas e irlas guardando en un array de arrays ¿no?, con este método bastaría. Pero eso no es exactamente un buffer. Serían varios.

    Es decir, buffer ya es lo que usamos con fgets.

    Hombre, no sé si me quieres decir algo como leer una cantidad determinada de bytes de la entrada. Eso lo puedes hacer directamente con fread sobre stdin, pero ojo, esto es algo muy crudo y para leer cadenas… tela, tendrías que poner el último byte del buffer a cero si quieres mostrarla.


  11. toda la info sobre las posibilidades de scanf estan en el manual de gnu c, seccion:

    12.14.5 String Input Conversions

    Pero yo lo encontré buscando por internet… lo de mirar el manual lo hice despues.
    Ya se sabe lo que se dice, unas pocas horas de ensayo-error te ahorrarán minutos de mirar manuales.


  12. Hola de nuevo, la idea que tengo es que lea n cantidad de texto y almacenarla en el buffer, para después mandarla a un file o no.

    La duda aquí es que no sé de que tamaño tendrá el texto que venga (viene de stdin) así que necesito un arreglo dinámico, o no?

    Dejo el código que tengo, igual y con eso me explico un poco:

    #include
    #include

    int main()
    {
    char c;
    int i=0;
    char resp;
    char title[50];
    char coso[10];
    char *texto;
    FILE *file;

    while((c=getchar()) != ‘~’)
    {
    coso[i]=c;
    i++;
    }
    printf(“\n———————————–\n”);
    printf(“El texto es: %s\n”, coso);
    printf(“\n———————————–\n”);
    printf(“Desea guardar el archivo? [S/N]\n”);
    resp=getchar();
    scanf(“%c”, &resp);
    resp=toupper(resp);
    switch(resp)
    {
    case ‘S':
    printf(“Nombre del archivo: “);
    scanf(“%s”,title);
    break;
    case ‘N':
    break;
    default: break;
    }
    return 0;
    }


  13. Hola! \n He intentado documentarme lo mejor que he podido al respecto, leyendo este manual y otros escritos pero supongo que sigo sin entenderlo, ya que no he podido hacer funcionar el siguiente *.c correctamente..jeje A ver si podeis orientarme.

    He probado con scanf, fgets,gets, utilizando setbuf(stdin, NULL); también con la funcion flush_in(); definida abajo y nada… entiendo que lea el ENTER como caracter en el bufer pero, como debo hacerlo bien entonces?
    Trabajo con cygwin actualizado.

    #include
    #include
    #include

    struct structCoche {
    char matricula[7];
    char marca[20];
    char modelo[20];
    char color[20];
    int estado; //0 = no disponible. 1 = disponible
    };
    typedef struct structCoche* Coche;

    Coche nuevoCoche() {
    Coche c = (Coche)malloc(sizeof(struct structCoche));
    int estado=1; //disponible
    return c;
    }

    void flush_in() {
    int ch;
    while ((ch = fgetc(stdin)) != EOF && ch != ‘\n’ ){}
    }

    int main() {
    int opcion;
    Coche nuevo = nuevoCoche();

    printf(“\n Opción: “);
    scanf(“%d”, &opcion); //variable de un switch

    printf(“\n\tMatrícula (ej:1234abc): “);
    fgets (nuevo->matricula, 7, stdin);
    printf(“\nchivato matricula2:%s:\n”,nuevo->matricula);

    printf(“\n\tMarca: “);
    fgets (nuevo->marca, 20, stdin);
    printf(“\n\nchivato marca:%s:”,nuevo->marca);
    printf(“\nchivato matricula2:%s:\n”,nuevo->matricula);

    printf(“\n\tModelo: “);
    fgets (nuevo->modelo, 20, stdin);

    printf(“\n\tColor: “);
    fgets (nuevo->color, 20, stdin);

    fprintf(stdout,”\n%s\t%s\t%s\t%s\t”,nuevo->matricula,nuevo->marca,nuevo->modelo,nuevo->color);
    if(nuevo->estado==1) fprintf(stdout,”Disponible”);
    else fprintf(stdout,”No disponible”);

    return 0;
    }


    • #include
      #include
      #include

      struct structCoche {
      char matricula[7];
      char marca[20];
      char modelo[20];
      char color[20];
      int estado; //0 = no disponible. 1 = disponible
      };
      typedef struct structCoche* Coche;

      Coche nuevoCoche() {
      Coche c = (Coche)malloc(sizeof(struct structCoche));
      c->estado=1; //disponible
      return c;
      }
      .
      .
      .
      nose de que se trata bien el programa pero haber hazle estas modificaciones


  14. Exelente explicacion….felicitaciones………..realmente esa informacionnn no la he visto por ahi….y sirve de mucho…….aunq necesito una ayuda……….sabes que introduzco un int con scanf, y luego intento introducir una cadena desde teclado………..el programa no me quiere leer la cadena cuando lo corro…x q sera???


  15. también se puede leer getline(cadena,tamanho,stdin)
    además esta función te redefine el tamaño de cadena si hace falta, por tanto da igual lo que le pases….pero se le suele poner la cadena=NULL, tamanho=0, dentro de un while para ir leyendo linea a linea.
    Así, cada vez que entra “la función” hace un malloc y asigna el tamaño que necesita para cada linea según lo que halla leido…pero hay que tener cuidado y liberar con free(), ya que getline hace la reserva de memoria por nosotros, pero nosotros tenemos que hacer la liberación.


  16. A rixy:

    El fallo es que según leo en las páginas de manual:

    #define _GNU_SOURCE
    #include

    Lo cual quiere decir que es una extensión de GNU y no se debería usar en aplicaciones que se pretendan portables. Sin embargo esta no me la sabía, y me la apunto, me va a ahorrar bastante código redundante en el futuro. Gracias por el aporte :)


  17. Hola ! muy buena informacion! me sirvio demas jeje.. pero me dio un problemita tambien q seguro es super tonto.. si quiero insertar por teclado 2 o mas datos, como por ejemplo nombre y apellido, y que me lea primero uno y luego el otro, no se, no me quiere leer el primero, como que se lo salta.. porq sera ?
    ejemplo:
    printf(“nombre : “);
    fgets(x,100,stdin);
    printf(“apellido : “);
    fgets(y,100,stdin);
    (eso lo tengo dentro de un case, yo hice una prueba aparte en otro programita y funciono perfecto, pero en ese case no lo hace :S Se me salta el nombre, osea q no puedo introducirlo, solo me pide el apellido)
    AYUDAAA
    gracias


  18. Hola migos buscando por la red me econtre con este post, tengo un problema y como soy muy novato con esto de linux no puedo hacer una programa medio pequeño, lo que necesito es crear un programa que me muestre en pantalla el numero de procesos que tiene la maquina es algo como ejecutar el comando ps en modo de comendos sera que alguien me da una mano gracias


  19. muchas gracias! me solucionaste ese problema! ;D


  20. Para limpiar el buffer podemos usar siempre antes de leer un fflush(stdin).

    Muy buena la guía, gracias.


  21. hola creo que llege un poquito tarde!!!

    bueno mi pregunta es la siguiente

    como se hace cuando no sabes el tamaño del texto que van a introducir por teclado;

    por ejemplo si se pide que introduzcas tu nombre 100 veces

    mi nombre tiene 4 letras X 100 = 400;
    y sin contar los espacios entre cada ves.

    pero cuando se introdusca otro nombre
    segundo nombre tiene 6 0 8 letras X 100 = 600 o 800

    #include

    int main ()
    {
    char nombre [400];
    printf (“Introduzca su nombre 100 veces: “);
    fgets (nombre, 400, stdin);
    printf (“ls lista de nombres es: “%s”n”, nombre);
    return 0;
    }

    que es lo que se puede hacer en este caso tan simple ?


  22. Gracias y mil gracias!!!!, me ha sido de gran ayuda.


  23. NO MANCHES, tuve un orgasmo jahjahjahjahjah, muchas gracias por la explicacion ni mi maestro de programacion lo pudo hacer mejor :D


  24. gracias por la explicacion pero como salgo del ciclo cuando ya no quiero que lea más???

    while (fgets (cadena, TAMANHO, stdin))
    {
    /* Cosas mientras leo líneas */
    }


  25. Webesito, quiza te pueda ayudar.

    char i;
    do{
    fgets(cadena, tamanho, stdin);
    printf(“Desea que siga leyendo?\n1.Si\n Presione cualquier otra tecla para continuar”);
    scanf(“%c”, &i);
    }while (i=’1′)


  26. Una excelente explicación
    Me ha sido de gran utilidad para resolver ciertos problemas de desborde que he tenido con mis programas.


  27. Good, bueno el articulo…yo quisiera saber tambien si alguien sabe como hostias puedo agregar espacios al inicio de un fichero…
    Tengo un problema con eso que necesita ser resuelto gg…si alguien sabe y puede responder urgente se lo agradeceria un millon…


  28. Several of these games are worth some time and are actually eceagkgkdgdg


  29. Hello, Neat post. There’s a problem with your web site in web explorer, may test thisK IE still is the market leader and a large component to people will leave out your magnificent writing due to this problem. aeeaekdcfbbf



Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.

A %d blogueros les gusta esto: