A (not so) minimal implementation of ‘cat’ in C

No particular reason but I needed something a little more complicated than the ubiquitous Hello World, so I opted for a minimal implementation of cat.

/*
 * cat.c
 *
 * Concatenate files and print on the standard output.
 * 
 * This  program is free software: you can redistribute it and/or modify  it
 * under  the  terms of the GNU General Public License as published  by  the
 * Free  Software  Foundation, either version 3 of the License, or (at  your
 * option) any later version.
 *
 * This  program  is  distributed in the hope that it will  be  useful,  but
 * WITHOUT   ANY   WARRANTY;   without  even   the   implied   warranty   of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
 * Public License for more details.
 *
 * You  should have received a copy of the GNU General Public License  along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * 03 Mar 14   0.1   - Initial version - MEJT
 * 03 Apr 18   0.2   - Updated error message - MEJT
 */
 
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
 
int main(int argc, char *argv[]) {
  FILE *file;
  int chr;
  int count;
 
  for(count = 1; count < argc; count++) {
    if((file = fopen(argv[count], "r")) == NULL) {
      fprintf(stderr, "%s: %s : %s\n", argv[0], argv[count],
        strerror(errno));
    continue;
    }
    while((chr = getc(file)) != EOF)
      fprintf(stdout, "%c", chr);
    fclose(file);
  }
  exit(0);
}

A couple of iterations and several years later I’ve added a bit lot more functionality. This now makes a pretty good template for any program that needs to parse command line options (see note below), and I have tested it on several different platforms with different versions of the C compiler.

/*
 *  cat -- Concatenate files and print on the standard output.
 *
 *  Copyright(C) 2019   MEJT
 *
 *  A not quite so minimal implementation of the ubiquitous 'cat'.
 *
 *  Deliberatly parses the command line without using 'getopt' or 'argparse'
 *  to maximize portability.
 *
 *  This  program is free software: you can redistribute it and/or modify it
 *  under  the terms of the GNU General Public License as published  by  the
 *  Free  Software Foundation, either version 3 of the License, or (at  your
 *  option) any later version.
 *
 *  This  program  is distributed in the hope that it will  be  useful,  but
 *  WITHOUT   ANY   WARRANTY;   without even   the   implied   warranty   of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 *  Public License for more details.
 *
 *  You  should have received a copy of the GNU General Public License along
 *  with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  03 Mar 14   0.1   - Initial version - MEJT
 *  03 Apr 18   0.2   - Changed error message - MEJT
 *  04 Apr 18   0.3   - Added some command line options including the option
 *                      to restart line numbering at the start of every file
 *                      add a delay when printing each character - MEJT
 *  08 Mar 19   0.4   - Removed  delay  option and rewrote argument  parsing
 *                      code to comply with the ANSI C standard - MEJT
 *                    - Changed flags to boolean values - MEJT
 *                    - Does NOT insert a newline between files - MEJT
 *  10 Mar 19         - Added  an  option to print the filename as a  header
 *                      before displaying the file - MEJT
 *                    - Allows  standard input to be specified as the  input
 *                      stream, implementing this change involved moving the
 *                      code  to  display the contents of each file  into  a
 *                      seperate routine - MEJT
 *                    - Implemented conditional compilation options for unix
 *                      and other platforms so delimiter for the the command
 *                      line options is correct - MEJT
 *  12 Mar 19         - Defined boolean values as integers and changed error
 *                      message  on  file open to allow code to be  compiled
 *                      with older compilers - MEJT
 *                    - Removed conditional compilation options - MEJT
 *                    - Fixed  bug  in option parsing routine to allow  more
 *                      than  one  option  to  be  specified  in  a   single
 *                      parameter - MEJT
 *                    - Prints header after the file is opened - MEJT
 *                    - Updated description - MEJT
 *  14 Mar 19         - Uses  errno to print error message (it is  necessary
 *                      to  assign errno to a local vairable before  calling
 *                      strerror(), don't really know why but this does seem
 *                      to depend on the compiler!) - MEJT
 *  16 Mar 19         - Fixed a bug in the option parser that any characters
 *                      immediatly  following the '--' were ignored and were
 *                      not treated as an invalid option - MEJT
 *                    - Mofified DEBUG macro - MEJT
 *                    - Option delimiter no longer defined in a macro - MEJT
 *                    - Added the capability to parse long options including
 *                      those that are only partly complete and used this to
 *                      add the ability to display the program version using
 *                      a macro to return the copyright year - MEJT
 *                    - Added all the long versions of each option including
 *                      '--version'  and  '--help'  and modified  the  error
 *                      messages  to suggest using '--help' when an  invalid
 *                      command line option is specified - MEJT
 *                    - Removed DATE macro - MEJT
 *
 *  To Do:            - Default to copying standard input to standard output
 *                      if no arguments are specified on the command line.
 *                    - Warn user if a command line option is ambiguous.
 */
 
#define VERSION       "0.4"
#define BUILD         "0021"
#define AUTHOR        "MEJT"
#define COPYRIGHT     (__DATE__ +7/* Extract copyright year from date */
 
#define DEBUG(code)   do {if (__DEBUG__) {code;}} while(0)
#define __DEBUG__  0
 
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
 
static int bflag = 0, hflag = 0, nflag = 0, rflag = 0, sflag = 0;
static int line = 0;
static int chr, prev, blank;
 
int cat(FILE *file) {
  blank = 0;
  if (rflag) line = 0;
  while ((chr = getc(file)) != EOF) {
    if (prev == '\n') {
      ++blank;
      if (sflag && chr == '\n' && blank > 1)
        continue;
      if (nflag)
        if (!bflag || chr != '\n')
          fprintf(stdout, "%6d\t", ++line);
    } 
    else 
      blank = 0;
    fprintf(stdout, "%c", chr);
    prev = chr;
  }
  return(fflush(stdout));
}
 
int main(int argc, char **argv) {
  FILE *file;
  int errnum;
  int count, index;
  int flag = 1;
 
  for (index = 1; index < argc && flag; index++) {
    if (argv[index][0] == '-') {
      count = 1;
      while (argv[index][count] != 0) {
        switch (argv[index][count]) {
          case 'b'/* Number non empty lines */
            nflag = 1; bflag = 1break;
          case 'h'/* Print filenames headings */
            hflag = 1break;          
          case 'n'/* Number lines */
            nflag = 1break;
          case 'r'/* Restart numbering */
            rflag = 1; nflag = 1break;
          case 's'/* Squeeze blank lines */
            sflag = 1break;
          case '-'/* '--' terminates command line processing */
            DEBUG(fprintf(stderr, "%s:%0d: argv[%0d][%0d]: '%c'\n", argv[0], __LINE__, index, count, argv[index][count]));
            count = strlen(argv[index]);
            if (count == 2
              flag = 0/* '--' terminates command line processing */
            else
              if (!strncmp(argv[index], "--version", count)) { /* Display version information */
                fprintf(stderr, "%s: Version %s\n", argv[0], VERSION);
                fprintf(stderr, "Copyright(C) %s %s\n", COPYRIGHT, AUTHOR);
                fprintf(stderr, "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.\n");
                fprintf(stderr, "This is free software: you are free to change and redistribute it.\n");
                fprintf(stderr, "There is NO WARRANTY, to the extent permitted by law.\n");
                exit(0);
              }
              else if (!strncmp(argv[index], "--number-nonblank", count)) {
                nflag = 1; flag = 1;
              }
              else if (!strncmp(argv[index], "--number", count)) {
                nflag = 1;
              }
              else if (!strncmp(argv[index], "--squeeze-blank", count)) {
                sflag = 1;
              }
              else if (!strncmp(argv[index], "--restart-numbering", count)) {
                rflag = 1;
              }
              else if (!strncmp(argv[index], "--show-filenames", count)) {
                hflag = 1;
              }
              else if (!strncmp(argv[index], "--help", count)) {
                fprintf(stdout, "Usage: %s [OPTION]... [FILE]...\n", argv[0]);
                fprintf(stdout, "Concatenate FILE(s), or standard input, to standard output.\n\n");
                fprintf(stdout, "  -b, --number-nonblank    number nonempty output lines, overrides -n\n");
                fprintf(stdout, "  -n, --number             number all output lines\n");
                fprintf(stdout, "  -r, --restart-numbering  start numbering each line in a file from one\n");
                fprintf(stdout, "  -s, --squeeze-blank      suppress repeated empty output lines\n");
                fprintf(stdout, "      --help               display this help and exit\n");
                fprintf(stdout, "      --version            output version information and exit\n\n");
                fprintf(stdout, "With no FILE, or when FILE is -, read standard input.\n");
                exit(0);
              }
              else { /* If we get here then the we have an invalid long option */
                fprintf(stderr, "%s: invalid option %s\n", argv[0], argv[index]);
                fprintf(stderr, "Try '%s --help' for more information.\n", argv[0]);
                exit(-1);
              }
            count--;
            break;
          default/* If we get here the single letter option is unknown */
            fprintf(stderr, "%s: unknown option -- %c\n", argv[0], argv[index][count]);
            fprintf(stderr, "Try '%s --help' for more information.\n", argv[0]);
            exit(-1);
        }
        count++;
      }
      if (argv[index][1] != 0) {
        for (count = index; count < argc - 1; count++) argv[count] = argv[count + 1];
        argc--; index--;
      }
    }
  }
 
  prev = '\n';
  for (count = 1; count < argc; count++)
    if (strcmp("-", argv[count]) == 0) {
      if (hflag) {
        if (prev != '\n'fprintf(stdout, "\n");
        fprintf(stdout, "stdin:\n");
        prev = '\n';
      }
      cat(stdin);
    }
    else {
      if ((file = fopen(argv[count], "r")) != NULL) {
        if (hflag) {
          if (prev != '\n'fprintf(stdout, "\n");
          fprintf(stdout, "%s:\n", argv[count]);
          prev = '\n';
        }
        cat(file);
        fclose(file);
      }
      else {
        errnum = errno;
        fprintf(stderr, "%s: %s: %s\n", argv[0], argv[count], strerror(errnum));      
      }
    }
  exit(0);
}

Note that I’ve deliberately NOT used getopt or argp as they are not part of the ANSI C standard.

Advertisements
This entry was posted in Programming and tagged . Bookmark the permalink.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.