Читать из файла или стандартного ввода - PullRequest
27 голосов
/ 16 августа 2010

Я пишу утилиту, которая принимает либо имя файла, либо читает из стандартного ввода.

Я хотел бы знать, какой самый надежный / быстрый способ проверить, существует ли stdin (данные передаются в программу) и, если это так, считывает эти данные. Если он не существует, обработка будет иметь место на имя файла дано. Я попытался использовать следующий тест для размера stdin, но я считаю, что, поскольку это поток, а не фактический файл, он не работает, как я подозревал, и всегда печатает -1. Я знаю, что всегда мог прочитать вводимые 1 символ за раз, в то время как! = EOF, но я бы хотел более общее решение, чтобы я мог получить либо fd, либо FILE *, если stdin существует, поэтому остальная часть программы будет работать без проблем , Я также хотел бы иметь возможность узнать его размер, пока поток не был закрыт предыдущей программой.

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);
}

Терминал:

$ echo "hi!" | myprog
Size of stdin: -1

Ответы [ 6 ]

22 голосов
/ 16 августа 2010

Вы думаете, что это неправильно.

Что вы пытаетесь сделать:

Если stdin существует, используйте его, иначе проверьте, предоставил ли пользователь имя файла.

Что вы должны делать вместо этого:

Если пользователь указывает имя файла, используйте его. Остальное использовать stdin.

Вы не можете знать общую длину входящего потока, пока не прочитаете все и не сохраните его в буфере. Вы просто не можете искать в трубах. Это ограничение работы труб. Трубы не подходят для всех задач, и иногда требуются промежуточные файлы.

15 голосов
/ 16 августа 2010

Сначала попросите программу сообщить вам, что не так, проверив errno, установленный при сбое, например, во время fseek или ftell.

Другие (tonio & LatinSuD)объяснил ошибку с обработкой стандартного ввода по сравнению с проверкой имени файла.А именно, сначала проверьте argc (количество аргументов), чтобы увидеть, указаны ли какие-либо параметры командной строки if (argc > 1), рассматривая - как особый случай, означающий stdin.

Если параметры не указаны, то предположим, что входные данные (собираются) поступят из stdin, который является потоком , а не файлом, и функция fseek завершается с ошибкой.

В случае потока, где вы не можете использовать библиотечные функции, ориентированные на файл (например, fseek и ftell), вам просто нужно подсчитать количество прочитанных байтов (включая завершающие символы новой строки).) до получения EOF (конец файла).

Для использования с большими файлами вы можете ускорить его, используя fgets для массива char для более эффективного чтения байтовв (текстовом) файле.Для двоичного файла вам нужно использовать fopen(const char* filename, "rb") и использовать fread вместо fgetc/fgets.

Вы также можете проверить для feof(stdin) / ferror(stdin) при использовании метода подсчета байтов для обнаружениялюбые ошибки при чтении из потока.

Пример ниже должен быть совместим с C99 и переносимым.

#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;
}
5 голосов
/ 16 августа 2010

Вы можете посмотреть, например, как это делается в утилите cat.

См. Код здесь .Если в качестве аргумента отсутствует имя файла или это «-», тогда для ввода используется stdin.stdin будет там, даже если на него не будет передано никаких данных (но тогда ваш вызов на чтение может ждать вечно).

4 голосов
/ 16 августа 2010

Вы можете просто читать из стандартного ввода, если пользователь не предоставит имя файла?

Если нет, обработайте специальное «имя файла» - как означающее «читать из стандартного ввода». Пользователь должен будет запустить программу наподобие cat file | myprogram -, если он хочет передать данные в нее, и myprogam file, если он хочет, чтобы она прочитала файл.

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
    }

Если вы используете * nix, вы можете проверить, является ли stdin fifo:

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

Хотя это не сработает, если пользователь сделает myprogram <file

Вы также можете проверить, является ли stdin терминалом / консолью

if(isatty(0)) {
  //stdin is a terminal
}
0 голосов
/ 18 декабря 2013

Обратите внимание, что вам нужно знать, подключен ли stdin к терминалу или нет, а не существует ли он.Он всегда существует, но когда вы используете оболочку для передачи чего-либо в него или чтения файла, он не подключается к терминалу.

Вы можете проверить, что дескриптор файла подключен к терминалу через termios.hfunctions:

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

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

Это попытается получить атрибуты терминала стандартного ввода.Если он не подключен к каналу, он подключен к tty, и вызов функции tcgetattr будет успешным.Чтобы обнаружить канал, мы проверяем на наличие ошибки tcgetattr.

0 голосов
/ 16 августа 2010

Я думаю, что просто проверка конца файла с feof подойдет.

...