The joy of manual options parsing in C

It’s written, it compiles, but it’s not tested yet.

There’s something about this kind of repetitive code. Repetitive, wordy, lengthy, error-prone. You could use an API like getopt which provides a convenient abstraction, but if you do so you lose that very fine degree of control over things like error messages and conflict-handling.

You could try to identify the common code and extract it out into a function (or, ugh, a macro) without losing any of the flexibility and control, but you run the risk of increasing complexity (possibly adding bugs) and perhaps harming readability.

If you’re writing a tool like Git with jillions of subcommands and a zillion options then it probably makes sense to pursue this kind of abstraction, but for a write-once command-line tool then it’s probably best to just do the unpleasant work, write some tests to mark sure the darn thing actually works, and forget about it.

NSArray *parse_options_or_die(options_t *options, int argc, char *argv[])
{
    int errors = 0;
    BOOL scanning_paths = NO;
    NSMutableArray *paths = [NSMutableArray array];
    for (int i = 1; i < argc; i++)
    {
        if (scanning_paths)
            [paths addObject:[NSString stringWithUTF8String:argv[i]]];
        else if (!strcmp(argv[i], "--"))
            scanning_paths = YES;
        else if (!strcmp(argv[i], "-r") || !strcmp(argv[i], "--recurse"))
        {
            if (options->recurse == -1)
            {
                report_conflicting_options(argv[i], "no-recurse");
                errors++;
            }
            else
                options->recurse = 1;
        }
        else if (!strcmp(argv[i], "-R") || !strcmp(argv[i], "--no-recurse"))
        {
            if (options->recurse == 1)
            {
                report_conflicting_options(argv[i], "recurse");
                errors++;
            }
            else
                options->recurse = -1;
        }
        else if (!strcmp(argv[i], "-e") || !strcmp(argv[i], "--regular-expression"))
        {
            if (options->regex == -1)
            {
                report_conflicting_options(argv[i], "no-regular-expression");
                errors++;
            }
            else
                options->regex = 1;
        }
        else if (!strcmp(argv[i], "-E") || !strcmp(argv[i], "--no-regular-expression"))
        {
            if (options->regex == 1)
            {
                report_conflicting_options(argv[i], "regular-expression");
                errors++;
            }
            else
                options->regex = -1;
        }
        else if (!strcmp(argv[i], "-i") || !strcmp(argv[i], "--case-insensitive") ||
                 !strcmp(argv[i], "--no-case-sensitive"))
        {
            if (options->case_insensitive == -1)
            {
                report_conflicting_options(argv[i], "case-sensitive");
                errors++;
            }
            else
                options->case_insensitive = 1;
        }
        else if (!strcmp(argv[i], "-I") || !strcmp(argv[i], "--no-case-insensitive") ||
                 !strcmp(argv[i], "--case-sensitive"))
        {
            if (options->case_insensitive == 1)
            {
                report_conflicting_options(argv[i], "case-insensitive");
                errors++;
            }
            else
                options->case_insensitive = -1;
        }
        else if (!strcmp(argv[i], "-g") || !strcmp(argv[i], "--replace-all"))
        {
            if (options->global == -1)
            {
                report_conflicting_options(argv[i], "no-replace-all");
                errors++;
            }
            else
                options->global = 1;
        }
        else if (!strcmp(argv[i], "-G") || !strcmp(argv[i], "--no-replace-all"))
        {
            if (options->global == 1)
            {
                report_conflicting_options(argv[i], "replace-all");
                errors++;
            }
            else
                options->global = -1;
        }
        else if (!strcmp(argv[i], "-p") || !strcmp(argv[i], "--preview"))
        {
            if (options->preview == -1)
            {
                report_conflicting_options(argv[i], "no-preview");
                errors++;
            }
            else
                options->preview = 1;
        }
        else if (!strcmp(argv[i], "-P") || !strcmp(argv[i], "--no-preview"))
        {
            if (options->preview == 1)
            {
                report_conflicting_options(argv[i], "preview");
                errors++;
            }
            else
                options->preview = -1;
        }
        else if (!strcmp(argv[i], "-u") || !strcmp(argv[i], "--gui"))
        {
            if (options->gui == -1)
            {
                report_conflicting_options(argv[i], "no-gui");
                errors++;
            }
            else
                options->gui = 1;
        }
        else if (!strcmp(argv[i], "-U") || !strcmp(argv[i], "--no-gui"))
        {
            if (options->gui == 1)
            {
                report_conflicting_options(argv[i], "gui");
                errors++;
            }
            else
                options->gui = -1;
        }
        else if (!strcmp(argv[i], "-d") || !strcmp(argv[i], "--dry-run"))
        {
            if (options->dry_run == -1)
            {
                report_conflicting_options(argv[i], "no-dry-run");
                errors++;
            }
            else
                options->dry_run = 1;
        }
        else if (!strcmp(argv[i], "-D") || !strcmp(argv[i], "--no-dry-run"))
        {
            if (options->dry_run == 1)
            {
                report_conflicting_options(argv[i], "dry-run");
                errors++;
            }
            else
                options->dry_run = -1;
        }
        else if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose"))
        {
            if (options->verbose == -1)
                options->verbose = 1;
            else
                options->verbose++;
        }
        else if (!strcmp(argv[i], "-V") || !strcmp(argv[i], "--no-verbose"))
        {
            if (options->verbose == 1)
                options->verbose = -1;
            else
                options->verbose--;
        }
        else if (!strcmp(argv[i], "-c"))
        {
            if (i + 1 == argc || argv[i + 1][0] == '-')
            {
                fprintf(stderr, "error: option -c requires an argument (-c file)\n");
                errors++;
            }
            else if (options->skip_conf_files == 1)
            {
                report_conflicting_options(argv[i], "no-conf");
                errors++;
            }
            else if (options->conf_path && strcmp(argv[i + 1], options->conf_path))
            {
                report_conflicting_options(argv[i], "conf");
                errors++;
            }
            else
            {
                i++;
                options->conf_path = argv[i];
            }
        }
        else if (!strncmp(argv[i], "--conf", 6))
        {
            if (options->skip_conf_files == 1)
            {
                report_conflicting_options(argv[i], "no-conf");
                errors++;
            }
            else if (argv[i][6] != '=' || strlen(argv[i]) < 8)
            {
                fprintf(stderr, "error: option --conf requires an argument (--conf=file)\n");
                errors++;
            }
            else if (options->conf_path && strcmp(argv[i] + 7, options->conf_path))
            {
                report_conflicting_options(argv[i], "conf");
                errors++;
            }
            else
                options->conf_path = argv[i] + 7;
        }
        else if (!strcmp(argv[i], "-C") || !strcmp(argv[i], "--no-conf"))
        {
            if (options->conf_path)
            {
                report_conflicting_options(argv[i], "conf");
                errors++;
            }
            else
                options->skip_conf_files = 1;
        }
        else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help"))
            show_usage_and_exit();
        else if (!strcmp(argv[i], "--version"))
            show_version_and_exit();
        else if (argv[i][0] == '-')
        {
            fprintf(stderr, "error: unrecognized option or argument: %s\n", argv[i]);
            errors++;
        }
        else
            [paths addObject:[NSString stringWithUTF8String:argv[i]]];
    }
    if (errors)
    {
        fprintf(stderr, "For usage information see: %s --help\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    return paths;
}