Behavior of program changing based on the order of option flags using getopt

Viewed 32

I am writing a cat command clone in C and I'm getting weird behavior when I change the order of my option flags.

The -s option flag squeezes double-spaced lines.

The -n option flag numbers every line starting at 1.

I've checked the difference of running my program in the following ways:

$ diff <(./myCat -s spaces.txt) <(cat -s spaces.txt)
no difference

$ diff <(./myCat -n spaces.txt) <(cat -n spaces.txt)
no difference

$ diff <(./myCat -ns spaces.txt) <(cat -ns spaces.txt)
no difference

running ./myCat -sn spaces.txt, however, only squeezes the text and does not number the lines. Can anyone explain this behavior? I thought the order of option flags did not matter in this case.

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

int main(int argc, char **argv) {

    FILE *fp;
    const int bufferSize = 4096;
    char buffer[bufferSize];
    int currentFile = 0;

    for (int i = 1; i < argc; i++) {
        if (argv[i][0] != '-') {
            currentFile = i;
            break;
        }
    }

    int bflag = 0, eflag = 0, nflag = 0, sflag = 0;
    int opt;

    while ((opt = getopt(argc, argv, "bens:?")) != -1) {
        switch(opt) {
          case 'b':
            bflag++;
            break;
          case 'e':
            eflag++;
            break;
          case 'n':
            nflag++;
            break;
          case 's':
             sflag++;
             break;
          case ':':
            printf("option needs a value\n");
            exit(1);
          case '?':
            printf("usage: cat [-bens] [file ...]\n");
            exit(1);
        }
    }
    
    while (currentFile < argc) {
        if (currentFile) {
            fp = fopen(argv[currentFile], "rb");
            if (fp == NULL) {
                fprintf(stderr, "%s: %s: No such file or directory",
                        argv[0], argv[currentFile]);
                exit(1);
            }
        }

        int lineNumber = 1;
        int lastLineBlank = 0;

        while (fgets(buffer, bufferSize, (fp == NULL ? stdin : fp))) {

            int length = strlen(buffer);
            buffer[length - 1] = '\0';

            if (sflag) {
                length = strlen(buffer);
                int currentLineBlank = (length <= 1) ? 1 : 0;
                if (lastLineBlank && currentLineBlank) {
                    continue;
                }
                lastLineBlank = currentLineBlank;
            }

            
            if (bflag) {
                length = strlen(buffer);
                if (length >= 1) {
                    char *tmp = strdup(buffer);
                    buffer[0] = '\0';
                    sprintf(buffer, "%*d\t", 6, lineNumber++);
                    strcat(buffer, tmp);
                }
            } else
            if (nflag) {
                char *tmp = strdup(buffer);
                buffer[0] = '\0';
                sprintf(buffer, "%*d\t", 6, lineNumber++);
                strcat(buffer, tmp);
            }

            if (eflag) {
                length = strlen(buffer);
                buffer[length] = '$';
                buffer[length + 1] = '\0';
            }

            fprintf(stdout, "%s\n", buffer);
        }

        fclose(fp);
        currentFile++;
    }

    return 0;
}
1 Answers

This is because of the optstring in getopt:

int getopt(int argc, char *const argv[], const char *optstring);

From the man page, the behaviour of optstring is:

optstring is a string containing the legitimate option characters. If such a character is followed by a colon, the option requires an argument, so getopt() places a pointer to the following text in the same argv-element, or the text of the following argv-element, in optarg. Two colons mean an option takes an optional arg; if there is text in the current argv- element (i.e., in the same word as the option name itself, for example, "-oarg"), then it is returned in optarg, otherwise optarg is set to zero.

The statement below means that only s requires an argument:

getopt(argc, argv, "bens:?")

(original code segfault on ./a.out -n and on ./a.out)

It looked like the only way was to use -ns or put -n after the filename. But the GNU libc example for getopt is pretty good - https://www.gnu.org/software/libc/manual/html_node/Example-of-Getopt.html
I rewrote a bit of the code so that it handles all the flags but still handles filenames properly. It is not perfect, but here it is:

// I removed the for loop before this. It is not required here.
// Please remove the for loop if you use this code.
// only the case ':' has been removed here 
while ((opt = getopt(argc, argv, "bens?")) != -1) {
    switch(opt) {
        case 'b':
            bflag++;
            break;
        case 'e':
            eflag++;
            break;
        case 'n':
            nflag++;
            break;
        case 's':
            sflag++;
            break;
        case '?':
            printf("usage: cat [-bens] [file ...]\n");
            exit(1);
    }
}

// optind is set by getopt. 
// It is equal to the position of the immediate argument after the options.
// this works because getopt permutes argv so that all the non-options are at the end
currentFile = optind;

// when no file name is provided
if(currentFile == argc) {
    printf("Need a filename!\n");
    return 1;
}

The rest of the code remains the same. It works for these examples:

  • ./a.out -sn file
  • ./a.out -ns file
  • ./a.out -s file1 file2
  • ./a.out -s file1 -n file2

This might still have bugs. Please do try it out and let me know if something is wrong.

Related