Conor Taylor
Conor Taylor

Reputation: 3108

Using getopt in C with non-option arguments

I'm making a small program in C that deals with a lot of command line arguments, so I decided to use getopt to sort them for me.

However, I want two non-option arguments (source and destination files) to be mandatory, so you have to have them as arguments while calling the program, even if there's no flags or other arguments.

Here's a simplified version of what I have to handle the arguments with flags:

while ((c = getopt(argc, argv, "i:d:btw:h:s:")) != -1) {
    switch (c) {
        case 'i': {
            i = (int)atol(optarg);
        }
        case 'd': {
            d = (int)atol(optarg);
        }
        case 'b':
            buf = 1;
            break;
        case 't':
            time = 1;
            break;
        case 'w':
            w = (int)atol(optarg);
            break;
        case 'h':
            h = (int)atol(optarg);
            break;
        case 's':
            s = (int)atol(optarg);
            break;
        default:
            break;
    }
}

How do I edit this so that non-option arguments are also handled?

I also want to be able to have the non-options either before or after the options, so how would that be handled?

Upvotes: 25

Views: 49256

Answers (7)

marooned
marooned

Reputation: 1

As per Langson.Leung's answer:

According to https://www.man7.org/linux/man-pages/man3/getopt.3.html

By default, getopt() permutes the contents of argv as it scans, so that eventually all the nonoptions are at the end. Two other scanning modes are also implemented. If the first character of optstring is '+' or the environment variable POSIXLY_CORRECT is set, then option processing stops as soon as a nonoption argument is encountered. If the first character of optstring is '-', then each nonoption argv-element is handled as if it were the argument of an option with character code 1. (This is used by programs that were written to expect options and other argv-elements in any order and that care about the ordering of the two.) The special argument "--" forces an end of option-scanning regardless of the scanning mode.

The contents of argv might indeed be permuted, but this is a GNU extension, technically not "POSIXLY_CORRECT", and:

  1. the specific way in which they are permuted is (probably) implementation-dependent. It appears that in the GNU/Linux case, this is done gradually, with non-option arguments "bubbling" up towards the end of argv; every time getopt() finds an option argument, it makes sure that all non-option arguments encountered thus far are placed exactly before the option argument found -- try printing the contents of argv after each getopt() invocation inside the loop and see what you get.

  2. generally, getopt() will skip over any arguments that do not match with the options provided. It will identify the rest correctly, wherever inside the argv they reside, and will return -1 when all arguments have been scanned (i.e. the end of argv is reached), with optind set to point at the first non-option argument, all of which are grouped together towards the end of argv in the order they were encountered. You can then process the non-option arguments after the loop, as already stated in previous answers.

If you want to process the non-option arguments on-the-fly, you would do something like this (see the comments in the code below!):

const char *src_file = NULL;
const char *dest_file = NULL;

/* notice the '-' character in front of the opstring arg to getopt() */
while ((c = getopt(argc, argv, "-i:d:btw:h:s:")) != -1) {
    switch (c) {
        /* parsed an argument that isn't among the
           provided options, getopt() returns 1 */
        case 1:
            /* do whatever needs to be done with the non-option argument;
               notice that optind is already incremented by getopt(),
               so we access argv @index optind-1 */
            printf("argv[%d]=%s\n", optind-1, argv[optind-1]);
            /* alternatively, since this non-option argument is treated
               as if it were an argument to an option with value 1,
               we can refer to it via optarg! */
            printf("%s\n", optarg);

            /* the following assumes the first non-option argument encountered
               to be the source file and the second non-option argument to be
               the destination file; all (if any) subsequent non-option
               arguments are ignored */
            if (src_file == NULL)
                src_file = optarg;
            else if (dest_file == NULL)
                dest_file = optarg;
            else 
                printf("src and dest files already set, arg ignored!\n");

            break;

        case 'i': {
            i = (int)atol(optarg);
        }
        case 'd': {
            d = (int)atol(optarg);
        }
        case 'b':
            buf = 1;
            break;
        case 't':
            time = 1;
            break;
        case 'w':
            w = (int)atol(optarg);
            break;
        case 'h':
            h = (int)atol(optarg);
            break;
        case 's':
            s = (int)atol(optarg);
            break;
        default:
            break;
    }
}

And then after the while loop, check if everything was provided:

if ( src_file == NULL || dest_file == NULL ) {
    printf("Mandatory argument(s) missing!\n");
    print_help();
    exit(1);
}

Explanation: By putting '-' in front of the opstring arg, we allow getopt() to scan the argv provided from left to right (no rearrangements performed) and return a positive integer value for each and every argument encountered, so we can handle them appropriately:

  • arguments that match one of the provided options return the value of the associated character
  • arguments that do begin with the '-' character but do not match any of the options provided are treated as unknown options and a message of the form <program_name>: invalid option -- '<character>' is implicitly printed by getopt(); they return the value of the character '?', so if we want special handling for them, we can add a case '?': to catch them :)
  • non-option arguments always return 1, so we can catch them with case 1: and handle them whichever way we please (e.g. on-the-fly as above, or we may put them in a separate buffer to process them later, outside of the getopt() loop etc)
  • after the last argument is parsed, a call to getopt() returns -1, and we exit the loop.

Alternatively: Compare with prefixing the opstring arg to getopt() with '+' (see manpage at top), where getopt() would return -1 at the first non-option argument encountered. We would exit the loop (possibly) early, but nothing can stop us from processing the offending argument, advancing the optind variable manually, and then resuming scanning from where it stopped!

while (optind < argc) {
    while ( (c = getopt("+...")) != -1 ) {
        ...
    }
    if (optind < argc) {
        printf("early -1 returned: optind=%d\n", optind);
        /* note that we cannot access the argument through the optarg
           variable in this case -- makes sense, it's not that we
           treat a non-option argument as if it were an argument to
           an option with value 1, as is the case with '-' mode
           scanning ;) */
        printf("argv[%d]=%s\n", optind, argv[optind]);
        /* handle argument */
        ...
        optind++;
    }
}

Portability: This is supposedly the "POSIXLY_CORRECT" way to parse the argument vector, so I would assume it works as expected in every POSIX-conforming environment, although I have no way of testing it.

Remember to remove the '+' prefix to the optstring arg, since this is a GNU extension:

The use of '+' and '-' in optstring is a GNU extension.

as per the manpage. If you are in a GNU environment, set the POSIXLY_CORRECT environment variable instead:

export POSIXLY_CORRECT=1

or simply run your program like so:

POSIXLY_CORRECT=1 ./program_name <argv>

Upvotes: 0

Klas Lindb&#228;ck
Klas Lindb&#228;ck

Reputation: 33273

getopt sets the optind variable to indicate the position of the next argument.

Add code similar to this after the options loop:

if (argv[optind] == NULL || argv[optind + 1] == NULL) {
  printf("Mandatory argument(s) missing\n");
  exit(1);
}

Edit:

If you want to allow options after regular arguments you can do something similar to this:

while (optind < argc) {
  if ((c = getopt(argc, argv, "i:d:btw:h:s:")) != -1) {
    // Option argument
    switch (c) {
    case 'i':
      i = (int)atol(optarg);
      break;
    case 'd':
      d = (int)atol(optarg);
      break;
    case 'b':
      buf = 1;
      break;
    case 't':
      time = 1;
      break;
    case 'w':
      w = (int)atol(optarg);
      break;
    case 'h':
      h = (int)atol(optarg);
      break;
    case 's':
      s = (int)atol(optarg);
      break;
    default:
      // bad or unknown option
      show_help();
      exit(1);
      break;
    }
  } else {
      // Regular argument
      <code to handle the argument>
      optind++;  // Skip to the next argument
  }
}

Upvotes: 38

Micrified
Micrified

Reputation: 3650

I'm going to go ahead and throw my answer in. I've tested it, and it complies with getopt as described here on Linux. Note, this answer only allows options first, then non-options after. But this is normal for a lot of CLI tools.

I'm adding this answer now because I found the EDIT in the answer from Klas did not work.


#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <ctype.h>


int main (int argc, char * const argv[])
{
    int c;
    
    // Process option arguments
    while (-1 != (c = getopt(argc, argv, "abc:"))) {
         switch (c) {
     case 'a': printf("option: a\n"); break;
     case 'b': printf("option: b\n"); break;
     case 'c': printf("option: c with arg: \"%s\"\n", optarg); break;
     default:
         printf("unknown arg: %2X\n", optopt);
     }
    }
    
    // Process remaining arguments
    for (int i = optind; i < argc; ++i) {
        printf("non-option: %s\n", argv[i]);
    }
    
    return EXIT_SUCCESS;
}

Upvotes: 0

Cheena
Cheena

Reputation: 1

The author of Mead's Guide to getopt states

If you want to have getopt parse and return the non-option arguments in the while loop (in the order specified), you must direct it to do so by putting a minus (-) in front of the optstring.

The example provided was "-:a:b:X" where the minus (-) "disables getopt from moving all non-option arguments to the end of the command line" and the colon (:) "disables getopt from displaying error messages".

If a non-option argument is found, then getopt will return an integer value of 1.

Upvotes: 0

Arpan Saini
Arpan Saini

Reputation: 5181

int main(int argc, char** argv) {
    
    
  char* inputfile;
  char* outputfile;
  char* output_file_type;
  char* color_red;
  char* color_blue;
  char* color_green;
  
  
  int opt;
  
  if (argv[optind] == NULL || argv[optind + 1] == NULL) {
  printf("Mandatory argument(s) missing\n");
  exit(1);
    }
 
  
  while((opt = getopt(argc, argv, ":i:o:r:g:b:t:")) != -1){
    switch(opt){
      case 'i':
        inputfile = optarg;
        printf("Input file : %s\n",inputfile);
        break;  
      case 'o':
        outputfile = optarg;
        printf("Output File: %s\n",outputfile);
        break;
      case 't':
        output_file_type = optarg;
        printf("Output File type: %s\n", output_file_type);
        break;
      case 'r':
        color_red = optarg;
        printf("Color Red: %s\n",color_red);
        break;
      case 'g':
        color_green = optarg;
        printf("Color Green: %s\n",color_green);
        break;
      case 'b':
        color_blue = optarg;
        printf("Color Blue: %s\n",color_blue);
        break;
      case ':':
        printf("option needs a value\n");
        break;
      case '?':
        printf("unknown option: %c\n", optopt);
        break;
    }
  
  }
  
  for (; optind < argc; optind++){
      
      
       printf("Given extra arguments: %s\n", argv[optind]);
     
   
  }  


    return (EXIT_SUCCESS);
}

Run commands:

gcc main.c -o image
./image -i ./resource/input_file.bmp -o ./resource/output_file.bmp -t BPM -r 10 -g 24 -b 40

output:

Input file : ./resource/input_file.bmp
Output File: ./resource/output_file.bmp
Output File type: BPM
Color Red: 10
Color Green: 24

Upvotes: 1

Langson.Leung
Langson.Leung

Reputation: 29

According to https://www.man7.org/linux/man-pages/man3/getopt.3.html

By default, getopt() permutes the contents of argv as it scans, so that eventually all the nonoptions are at the end. Two other scanning modes are also implemented. If the first character of optstring is '+' or the environment variable POSIXLY_CORRECT is set, then option processing stops as soon as a nonoption argument is encountered. If the first character of optstring is '-', then each nonoption argv-element is handled as if it were the argument of an option with character code 1. (This is used by programs that were written to expect options and other argv-elements in any order and that care about the ordering of the two.) The special argument "--" forces an end of option-scanning regardless of the scanning mode.

Upvotes: 2

Madars Vi
Madars Vi

Reputation: 1017

Really good example could be found here: GNU Libc The code:

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

int
main (int argc, char **argv)
{
int aflag = 0;
int bflag = 0;
char *cvalue = NULL;
int index;
int c;

opterr = 0;

while ((c = getopt (argc, argv, "abc:")) != -1)
switch (c)
{
case 'a':
    aflag = 1;
    break;
case 'b':
    bflag = 1;
    break;
case 'c':
    cvalue = optarg;
    break;
case '?':
    if (optopt == 'c')
    fprintf (stderr, "Option -%c requires an argument.\n", optopt);
    else if (isprint (optopt))
    fprintf (stderr, "Unknown option `-%c'.\n", optopt);
    else
    fprintf (stderr,
        "Unknown option character `\\x%x'.\n",
        optopt);
    return 1;
default:
    abort ();
}

printf ("aflag = %d, bflag = %d, cvalue = %s\n",
    aflag, bflag, cvalue);

for (index = optind; index < argc; index++)
printf ("Non-option argument %s\n", argv[index]);
return 0;
}

It allows to have options before and after arguments. I did compile and run test example:

$ ./a.out aa ff bb -a -ctestparam hello
aflag = 1, bflag = 0, cvalue = testparam
Non-option argument aa
Non-option argument ff
Non-option argument bb
Non-option argument hello

Upvotes: 17

Related Questions