Leer desde archivo o stdin


Estoy escribiendo una utilidad que acepta un nombre de archivo, o lee desde stdin.

Me gustaría saber la forma más robusta / rápida de comprobar si existe stdin (los datos están siendo canalizados al programa) y si es así leer esos datos en. Si no existe, el procesamiento se llevará a cabo en el nombre de archivo dado. He intentado usar la siguiente prueba para el tamaño de stdin pero creo que ya que es un flujo y no un archivo real, no está funcionando como sospechaba que lo haría y es siempre imprimiendo -1. Sé que siempre podría leer el carácter de entrada 1 a la vez mientras != EOF pero me gustaría una solución más genérica para que pudiera terminar con un fd o un ARCHIVO * si existe stdin para que el resto del programa funcione sin problemas. También me gustaría poder conocer su tamaño, a la espera de que el stream haya sido cerrado por el programa anterior.

long getSizeOfInput(FILE *input){
  long retvalue = 0;
  fseek(input, 0L, SEEK_END);
  retvalue = ftell(input);
  fseek(input, 0L, SEEK_SET);
  return retvalue;
}

int main(int argc, char **argv) {
  printf("Size of stdin: %ld\n", getSizeOfInput(stdin));
  exit(0);
}

Terminal:

$ echo "hi!" | myprog
Size of stdin: -1
Author: Tshepang, 2010-08-16

6 answers

Primero, pídale al programa que le diga lo que está mal marcando el errno, que se establece en caso de error, como durante fseek o ftell.

Otros (tonio y LatinSuD) han explicado el error con el manejo de stdin frente a la comprobación de un nombre de archivo. Es decir, primero verifique argc (recuento de argumentos) para ver si hay algún parámetro de línea de comandos especificado if (argc > 1), tratando - como un caso especial que significa stdin.

Si no se especifican parámetros, entonces asuma que la entrada viene (va) de stdin, que es un flujo no un archivo, y la función fseek falla en él.

En el caso de un flujo, donde no se pueden usar funciones de biblioteca orientadas a archivos en disco (es decir, fseek y ftell), simplemente tiene que contar el número de bytes leídos (incluidos los caracteres de nueva línea al final) hasta recibir EOF (fin del archivo).

Para el uso con archivos grandes, podría acelerarlo usando fgets a una matriz char para una lectura más eficiente de los bytes en un archivo (de texto). Para un archivo binario que necesita usar fopen(const char* filename, "rb") y usar fread en lugar de fgetc/fgets.

También puede comprobar la feof(stdin) / ferror(stdin) cuando se utiliza el método de conteo de bytes para detectar cualquier error al leer desde una secuencia.

La muestra siguiente debe ser compatible con C99 y portátil.

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

long getSizeOfInput(FILE *input){
   long retvalue = 0;
   int c;

   if (input != stdin) {
      if (-1 == fseek(input, 0L, SEEK_END)) {
         fprintf(stderr, "Error seek end: %s\n", strerror(errno));
         exit(EXIT_FAILURE);
      }
      if (-1 == (retvalue = ftell(input))) {
         fprintf(stderr, "ftell failed: %s\n", strerror(errno));
         exit(EXIT_FAILURE);
      }
      if (-1 == fseek(input, 0L, SEEK_SET)) {
         fprintf(stderr, "Error seek start: %s\n", strerror(errno));
         exit(EXIT_FAILURE);
      }
   } else {
      /* for stdin, we need to read in the entire stream until EOF */
      while (EOF != (c = fgetc(input))) {
         retvalue++;
      }
   }

   return retvalue;
}

int main(int argc, char **argv) {
   FILE *input;

   if (argc > 1) {
      if(!strcmp(argv[1],"-")) {
         input = stdin;
      } else {
         input = fopen(argv[1],"r");
         if (NULL == input) {
            fprintf(stderr, "Unable to open '%s': %s\n",
                  argv[1], strerror(errno));
            exit(EXIT_FAILURE);
         }
      }
   } else {
      input = stdin;
   }

   printf("Size of file: %ld\n", getSizeOfInput(input));

   return EXIT_SUCCESS;
}
 15
Author: mctylr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-16 16:58:55

Lo estás pensando mal.

Lo que estás tratando de hacer:

Si existe stdin úselo, de lo contrario compruebe si el usuario proporcionó un nombre de archivo.

Lo que deberías estar haciendo en su lugar:

Si el usuario proporciona un nombre de archivo, utilice el nombre de archivo. De lo contrario, use stdin.

No puede saber la longitud total de una transmisión entrante a menos que la lea toda y la mantenga en búfer. Simplemente no se puede buscar hacia atrás en las tuberías. Esta es una limitación de cómo funcionan las tuberías. Las tuberías no son adecuadas para todas las tareas y, a veces, se requieren archivos intermedios.

 20
Author: LatinSuD,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-26 09:45:39

Es posible que desee ver cómo se hace esto en la utilidad cat, por ejemplo.

Ver código aquí. Si no hay un nombre de archivo como argumento, o es "-", entonces stdin se usa para la entrada. stdin estará allí, incluso si no se envían datos a él (pero entonces, su llamada de lectura puede esperar para siempre).

 5
Author: tonio,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-16 16:22:43

¿Solo puede leer desde stdin a menos que el usuario proporcione un nombre de archivo ?

Si no, trate el especial "filename" - como "read from stdin". El usuario tendría que iniciar el programa como cat file | myprogram - si quiere canalizar datos a él, y myprogam file si quiere que se lea desde un archivo.

int main(int argc,char *argv[] ) {
  FILE *input;
  if(argc != 2) {
     usage();
     return 1;
   }
   if(!strcmp(argv[1],"-")) {
     input = stdin;
    } else {
      input = fopen(argv[1],"rb");
      //check for errors
    }

Si estás en *nix, puedes comprobar si stdin es un fifo:

 struct stat st_info;
 if(fstat(0,&st_info) != 0)
   //error
  }
  if(S_ISFIFO(st_info.st_mode)) {
     //stdin is a pipe
  }

Aunque eso no manejará al usuario haciendo myprogram <file

También puede comprobar si stdin es un terminal / consola

if(isatty(0)) {
  //stdin is a terminal
}
 4
Author: nos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-16 16:34:02

Solo probar el final del archivo con feof sería suficiente, creo.

 0
Author: Jens Gustedt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-16 16:19:24

Tenga en cuenta que lo que desea es saber si stdin está conectado a un terminal o no, no si existe. Siempre existe, pero cuando se utiliza el shell para canalizar algo en él o leer un archivo, no está conectado a un terminal.

Puede comprobar que un descriptor de fichero está conectado a un terminal a través de los termios.funciones h:

#include <termios.h>
#include <stdbool.h>

bool stdin_is_a_pipe(void)
{
    struct termios t;
    return (tcgetattr(STDIN_FILENO, &t) < 0);
}

Esto intentará obtener los atributos terminales de stdin. Si no está conectado a una tubería, está conectado a un tty y la llamada a la función tcgetattr éxito. Con el fin de detectar una tubería, comprobamos la falla tcgetattr.

 0
Author: user3114703,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-18 10:11:36