在C语言中解析命令行参数

156

我正在尝试编写一个程序,在C语言中可以逐行、逐词或逐字符地比较两个文件。它必须能够读取命令行选项-l-w-i-- ...

  • 如果选项是-l,则按行比较文件。
  • 如果选项是-w,则按单词比较文件。
  • 如果选项是--,则自动假定下一个参数是第一个文件名。
  • 如果选项是-i,则以不区分大小写的方式进行比较。
  • 默认按字符比较文件。

重要的是无论选项被输入多少次,只要-w-l没有同时输入,并且文件数量恰好为两个,就不会有问题。

我甚至不知道从哪里开始解析命令行参数。

所以这是我为所有事情想出来的代码。我还没有完全检查错误,但我写得过于复杂了吗?

/*
 * Functions to compare files.
 */
int compare_line();
int compare_word();
int compare_char();
int case_insens();

/*
 * Program to compare the information in two files and print message saying
 * whether or not this was successful.
 */
int main(int argc, char* argv[])
{
    /* Loop counter */
    size_t i = 0;

    /* Variables for functions */
    int caseIns = 0;
    int line = 0;
    int word = 0;

    /* File pointers */
    FILE *fp1, *fp2;

    /*
     * Read through command-line arguments for options.
     */
    for (i = 1; i < argc; i++)
    {
        printf("argv[%u] = %s\n", i, argv[i]);
        if (argv[i][0] == '-')
        {
             if (argv[i][1] == 'i')
             {
                 caseIns = 1;
             }
             if (argv[i][1] == 'l')
             {
                 line = 1;
             }
             if (argv[i][1] == 'w')
             {
                 word = 1;
             }
             if (argv[i][1] == '-')
             {
                 fp1 = argv[i][2];
                 fp2 = argv[i][3];
             }
             else
             {
                 printf("Invalid option.");
                 return 2;
             }
        }
        else
        {
           fp1(argv[i]);
           fp2(argv[i][1]);
        }
    }

    /*
     * Check that files can be opened.
     */
    if(((fp1 = fopen(fp1, "rb")) ==  NULL) || ((fp2 = fopen(fp2, "rb")) == NULL))
    {
        perror("fopen()");
        return 3;
    }
    else
    {
        if (caseIns == 1)
        {
            if(line == 1 && word == 1)
            {
                printf("That is invalid.");
                return 2;
            }
            if(line == 1 && word == 0)
            {
                if(compare_line(case_insens(fp1, fp2)) == 0)
                        return 0;
            }
            if(line == 0 && word == 1)
            {
                if(compare_word(case_insens(fp1, fp2)) == 0)
                    return 0;
            }
            else
            {
                if(compare_char(case_insens(fp1,fp2)) == 0)
                    return 0;
            }
        }
        else
        {
            if(line == 1 && word == 1)
            {
                printf("That is invalid.");
                return 2;
            }
            if(line == 1 && word == 0)
            {
                if(compare_line(fp1, fp2) == 0)
                    return 0;
            }
            if(line == 0 && word == 1)
            {
                if(compare_word(fp1, fp2) == 0)
                    return 0;
            }
            else
            {
                if(compare_char(fp1, fp2) == 0)
                    return 0;
            }
        }
    }
    return 1;

    if(((fp1 = fclose(fp1)) == NULL) || (((fp2 = fclose(fp2)) == NULL)))
    {
        perror("fclose()");
        return 3;
    }
    else
    {
        fp1 = fclose(fp1);
        fp2 = fclose(fp2);
    }
}

/*
 * Function to compare two files line-by-line.
 */
int compare_line(FILE *fp1, FILE *fp2)
{
    /* Buffer variables to store the lines in the file */
    char buff1 [LINESIZE];
    char buff2 [LINESIZE];

    /* Check that neither is the end of file */
    while((!feof(fp1)) && (!feof(fp2)))
    {
        /* Go through files line by line */
        fgets(buff1, LINESIZE, fp1);
        fgets(buff2, LINESIZE, fp2);
    }

    /* Compare files line by line */
    if(strcmp(buff1, buff2) == 0)
    {
        printf("Files are equal.\n");
        return 0;
    }
    printf("Files are not equal.\n");
    return 1;
}

/*
 * Function to compare two files word-by-word.
 */
int compare_word(FILE *fp1, FILE *fp2)
{
    /* File pointers */
    FILE *fp1, *fp2;

    /* Arrays to store words */
    char fp1words[LINESIZE];
    char fp2words[LINESIZE];

    if(strtok(fp1, " ") == NULL || strtok(fp2, " ") == NULL)
    {
        printf("File is empty. Cannot compare.\n");
        return 0;
    }
    else
    {
        fp1words = strtok(fp1, " ");
        fp2words = strtok(fp2, " ");

        if(fp1words == fp2words)
        {
            fputs(fp1words);
            fputs(fp2words);
            printf("Files are equal.\n");
            return 0;
        }
    }
    return 1;
}

/*
 * Function to compare two files character by character.
 */
int compare_char(FILE *fp1,FILE *fp2)
{
    /* Variables to store the characters from both files */
    int c;
    int d;

    /* Buffer variables to store chars */
    char buff1 [LINESIZE];
    char buff2 [LINESIZE];

    while(((c = fgetc(fp1))!= EOF) && (((d = fgetc(fp2))!=EOF)))
    {
        if(c == d)
        {
            if((fscanf(fp1, "%c", buff1)) == (fscanf(fp2, "%c", buff2)))
            {
                printf("Files have equivalent characters.\n");
                return 1;
                break;
            }
        }

    }
    return 0;
}

/*
 * Function to compare two files in a case-insensitive manner.
 */
int case_insens(FILE *fp1, FILE *fp2, size_t n)
{
    /* Pointers for files. */
    FILE *fp1, *fp2;

    /* Variable to go through files. */
    size_t i = 0;

    /* Arrays to store file information. */
    char fp1store[LINESIZE];
    char fp2store[LINESIZE];

    while(!feof(fp1) && !feof(fp2))
    {
        for(i = 0; i < n; i++)
        {
            fscanf(fp1, "%s", fp1store);
            fscanf(fp2, "%s", fp2store);

            fp1store = tolower(fp1store);
            fp2store = tolower(fp2store);

            return 1;
        }
    }
    return 0;
}

5
因此,去阅读关于它的手册页面吧;它并不是非常复杂,而且手册页面可能会包括一个供你实验的例子(如果你当地的手册页面没有,你肯定可以在网上找到例子)。 - Jonathan Leffler
2
这是一个高级库:argparse,使用起来非常简单。 - Cofyc
https://dev59.com/X3VC5IYBdhLWcg3wxEJ1 - jamesdlin
1
哇,这有很多strcmps :q - BarbaraKwarc
这个问题应该分成两个部分,一个是关于软件推荐的问题,可以在softwarerecs.stackexchange.com上提问;另一个是关于标准C库、glibc和POSIX等中的参数解析功能的小问题。 - undefined
显示剩余3条评论
15个回答

314
据我所知,在C语言中解析命令行参数最流行的三种方法是:
  • Getopt(来自POSIX C库的#include <unistd.h>),可以解决简单的参数解析任务。如果您对bash有些熟悉,那么bash内置的getopt是基于GNU libc的Getopt。
  • Argp(来自GNU C库的#include <argp.h>),可以解决更复杂的任务,并处理一些内容,例如:
    • -?--help用于帮助信息,包括电子邮件地址
    • -V--version用于版本信息
    • --usage用于使用信息
  • 自己编写,我不建议为了交给别人使用而自己编写程序,因为可能会出现太多问题或降低质量。忘记使用“--”停止选项解析的常见错误只是其中之一。
GNU C库文档提供了一些很好的Getopt和Argp示例。

使用Getopt的示例

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

int main(int argc, char *argv[])
{
    bool isCaseInsensitive = false;
    int opt;
    enum { CHARACTER_MODE, WORD_MODE, LINE_MODE } mode = CHARACTER_MODE;

    while ((opt = getopt(argc, argv, "ilw")) != -1) {
        switch (opt) {
        case 'i': isCaseInsensitive = true; break;
        case 'l': mode = LINE_MODE; break;
        case 'w': mode = WORD_MODE; break;
        default:
            fprintf(stderr, "Usage: %s [-ilw] [file...]\n", argv[0]);
            exit(EXIT_FAILURE);
        }
    }

    // Now optind (declared extern int by <unistd.h>) is the index of the first non-option argument.
    // If it is >= argc, there were no non-option arguments.

    // ...
}

使用Argp的示例

#include <argp.h>
#include <stdbool.h>

const char *argp_program_version = "programname programversion";
const char *argp_program_bug_address = "<your@email.address>";
static char doc[] = "Your program description.";
static char args_doc[] = "[FILENAME]...";
static struct argp_option options[] = { 
    { "line", 'l', 0, 0, "Compare lines instead of characters."},
    { "word", 'w', 0, 0, "Compare words instead of characters."},
    { "nocase", 'i', 0, 0, "Compare case insensitive instead of case sensitive."},
    { 0 } 
};

struct arguments {
    enum { CHARACTER_MODE, WORD_MODE, LINE_MODE } mode;
    bool isCaseInsensitive;
};

static error_t parse_opt(int key, char *arg, struct argp_state *state) {
    struct arguments *arguments = state->input;
    switch (key) {
    case 'l': arguments->mode = LINE_MODE; break;
    case 'w': arguments->mode = WORD_MODE; break;
    case 'i': arguments->isCaseInsensitive = true; break;
    case ARGP_KEY_ARG: return 0;
    default: return ARGP_ERR_UNKNOWN;
    }   
    return 0;
}

static struct argp argp = { options, parse_opt, args_doc, doc, 0, 0, 0 };

int main(int argc, char *argv[])
{
    struct arguments arguments;

    arguments.mode = CHARACTER_MODE;
    arguments.isCaseInsensitive = false;

    argp_parse(&argp, argc, argv, 0, 0, &arguments);

    // ...
}

自己动手的示例

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{   
    bool isCaseInsensitive = false;
    enum { CHARACTER_MODE, WORD_MODE, LINE_MODE } mode = CHARACTER_MODE;
    size_t optind;
    for (optind = 1; optind < argc && argv[optind][0] == '-'; optind++) {
        switch (argv[optind][1]) {
        case 'i': isCaseInsensitive = true; break;
        case 'l': mode = LINE_MODE; break;
        case 'w': mode = WORD_MODE; break;
        default:
            fprintf(stderr, "Usage: %s [-ilw] [file...]\n", argv[0]);
            exit(EXIT_FAILURE);
        }   
    }
    argv += optind;

    // *argv points to the remaining non-option arguments.
    // If *argv is NULL, there were no non-option arguments.

    // ...
}   

免责声明:我对Argp不熟悉,示例可能包含错误。

16
非常详尽的回答,感谢Christian(已点赞)。然而,Mac用户应该意识到,argp方法不具有跨平台兼容性。 根据我在这里找到的信息(http://lists.apple.com/archives/unix-porting/2006/Jun/msg00019.html),Argp是一个非标准化的glibc API扩展。它在gnulib中可用,因此可以明确地添加到项目中。但是,对于仅面向Mac或跨平台开发人员来说,使用getopt方法可能更简单。 - thclark
2
对于自己动手的版本,我不喜欢选项后面允许有额外的文本,例如-wzzz和-w解析相同,而且选项必须在文件参数之前。 - Jake
2
@Jake,你说得对。感谢你发现了这个问题。我不记得在写这段代码时是否注意到了这个问题。这再次证明了DIY很容易出错,因此不应该这样做。谢谢你告诉我,我可能会修复这个例子。 - Christian Hujer
1
这只是挑剔--*argv不指向剩余的非选项参数!应该添加argv += optind或类似的内容。正如@ChristianHujer所提到的那样,这是DIY易错的另一个例子。 - Jay Lee

24

使用getopt()函数,或者可能使用getopt_long()函数。

int iflag = 0;
enum { WORD_MODE, LINE_MODE } op_mode = WORD_MODE;  // Default set
int opt;

while ((opt = getopt(argc, argv, "ilw") != -1)
{
    switch (opt)
    {
    case 'i':
        iflag = 1;
        break;
    case 'l':
        op_mode = LINE_MODE;
        break;
    case 'w':
        op_mode = WORD_MODE;
        break;
    default:
        fprintf(stderr, "Usage: %s [-ilw] [file ...]\n", argv[0]);
        exit(EXIT_FAILURE);
    }
}

/* Process file names or stdin */
if (optind >= argc)
    process(stdin, "(standard input)", op_mode);
else
{
    int i;
    for (i = optind; i < argc; i++)
    {
        FILE *fp = fopen(argv[i], "r");
        if (fp == 0)
            fprintf(stderr, "%s: failed to open %s (%d %s)\n",
                    argv[0], argv[i], errno, strerror(errno));
        else
        {
            process(fp, argv[i], op_mode);
            fclose(fp);
        }
    }
 }
请注意,您需要确定哪些标头需要包含(我认为需要4个),我写的op_mode类型意味着您在process()函数中有一个问题——您无法在那里访问枚举。最好将枚举移动到函数外部;您甚至可以使op_mode成为没有外部链接的文件范围变量(一种花哨的方法是使用static)以避免将其传递给该函数。此代码不将-视为标准输入的同义词,这是读者的另一个练习。请注意,getopt()会自动处理--以标记选项的结束。
我还没有通过编译器运行上面的代码,其中可能存在错误。
为了额外的学分,请编写一个(库)函数:
int filter(int argc, char **argv, int idx, int (*function)(FILE *fp, const char *fn));

这段代码封装了getopt()循环后处理文件名选项的逻辑。它应该将-视为标准输入。注意,使用此函数意味着op_mode应该是静态文件范围内的变量。 filter()函数接受argcargvoptind以及一个指向处理函数的指针。如果成功打开了所有文件并且所有函数调用都返回0,则应该返回0(EXIT_SUCCESS),否则返回1(或EXIT_FAILURE)。使用这样的函数简化了编写类似Unix风格的过滤程序,可以在命令行或标准输入中读取指定的文件。


1
我不喜欢getopt()函数不允许在第一个文件之后添加选项。 - Jake
POSIX的getopt()默认不支持;GNU的getopt()默认支持。你可以选择其中一个。我不是很喜欢在文件名后面添加选项的行为,因为它在不同的平台上并不可靠。 - Jonathan Leffler
在MacOS中,getopt_long()函数位于getopt.h文件中,而getopt()函数位于unistd.h文件中。前者通过#include后者进行引用。 - Dennis Williamson
在MacOS中,getopt_long()getopt.h中,而getopt()unistd.h中。前者#include后者。 - undefined

22

我发现Gengetopt非常有用 - 你只需使用简单的配置文件指定所需选项,它就会生成一个.c/.h文件对,你只需将其包含并与应用程序链接即可。生成的代码使用getopt_long函数,似乎可以处理大多数常见的命令行参数,并且可以节省很多时间。

Gengetopt输入文件可能如下所示:

version "0.1"
package "myApp"
purpose "Does something useful."

# Options
option "filename" f "Input filename" string required
option "verbose" v "Increase program verbosity" flag off
option "id" i "Data ID" int required
option "value" r "Data value" multiple(1-) int optional 

生成代码很容易,并输出cmdline.hcmdline.c

$ gengetopt --input=myApp.cmdline --include-getopt
生成的代码很容易集成:
#include <stdio.h>
#include "cmdline.h"

int main(int argc, char ** argv) {
  struct gengetopt_args_info ai;
  if (cmdline_parser(argc, argv, &ai) != 0) {
    exit(1);
  }
  printf("ai.filename_arg: %s\n", ai.filename_arg);
  printf("ai.verbose_flag: %d\n", ai.verbose_flag);
  printf("ai.id_arg: %d\n", ai.id_arg);
  int i;
  for (i = 0; i < ai.value_given; ++i) {
    printf("ai.value_arg[%d]: %d\n", i, ai.value_arg[i]);
  }
}
如果您需要进行任何额外的检查(例如确保标志互斥),则可以使用存储在 gengetopt_args_info 结构中的数据轻松完成此操作。

如果你需要做任何额外的检查(比如确保标记是互斥的),你可以很容易地通过存储在gengetopt_args_info结构中的数据来完成这个任务。


1++除了生成警告的代码外,没有其他问题 :( - cat
是的,不幸的是。我在我的cmake文件中放置了异常。 - davidA
请注意,如果您重新生成源代码,您显然会失去它们,因此您可能希望在构建过程中将它们作为补丁应用。坦白地说,我发现直接关闭这些特定文件上的警告更容易。 - davidA
不,我的意思是将编译指示放在#include周围,而不是在生成的文件本身中。对我来说,关闭警告是禁止的 :-) - cat
啊,我误解你了。在我的设置中,我没有看到任何包含的头文件引发警告,但是当您#include它们时,您可以在那里使用有针对性的编译指示。实际上,我以为您指的是在编译生成的.c文件时编译器发出的警告,在我的情况下确实会发出警告。我只建议关闭这些生成文件的警告,而不是关闭您自己代码中的任何警告。 - davidA
显示剩余2条评论

7

2
@cat 你为什么认为它需要更新呢?这种对待软件的态度是错误的。 - Joshua Hedges
除非我想自己维护这个项目,否则我希望在我的活跃维护的代码中使用活跃维护的代码。有很多来自2006年的项目正在积极维护,但是这个项目已经死了,可能还存在着错误。而且,两年前(几乎完全相同!)我写下那些话已经是很久以前的事情了 :P - cat
3
因为opt已经完成且体积小巧,所以它并没有得到积极地维护。仅仅是为了好玩,我刚刚下载并尝试编译它(使用gcc-7.3),结果发现库构建成功并能够正常工作,但C++测试需要进行一些小修改。iostream.h 应该改为 iostream,并加上 using namespace std;。我会向James提出这个建议。这只影响C++ API测试,不影响代码本身。 - markgalassi
2
@cat 如果程序是在1963年编写的,而你传递给它2 + 2并返回4,那么谁会在意它是否被积极维护呢?我明白你可能正在积极更改你的GUI,其中90%的代码用于圆形按钮或其他内容,但这并不意味着解析命令行参数的程序在完美后需要得到积极维护。 - WinEunuuchs2Unix

7

Docopt有一个C语言实现,我认为它非常不错:

Docopt可以从标准化的man-page格式中描述命令行选项,并推断和创建参数解析器。这个项目最初是用Python编写的;Python版本只是简单地解析docstring并返回一个字典。在C语言中完成这个任务需要做更多的工作,但它很容易使用且没有外部依赖。


3

有一个非常好用的通用C库,libUCW,其中包括了简洁的命令行选项解析配置文件加载

该库还附带了良好的文档,并包含其他一些有用的内容(快速I/O、数据结构、分配器等),但这些也可以单独使用。

libUCW选项解析器示例(来自库文档)

#include <ucw/lib.h>
#include <ucw/opt.h>

int english;
int sugar;
int verbose;
char *tea_name;

static struct opt_section options = {
  OPT_ITEMS {
    OPT_HELP("A simple tea boiling console."),
    OPT_HELP("Usage: teapot [options] name-of-the-tea"),
    OPT_HELP(""),
    OPT_HELP("Options:"),
    OPT_HELP_OPTION,
    OPT_BOOL('e', "english-style", english, 0, "\tEnglish style (with milk)"),
    OPT_INT('s', "sugar", sugar, OPT_REQUIRED_VALUE, "<spoons>\tAmount of sugar (in teaspoons)"),
    OPT_INC('v', "verbose", verbose, 0, "\tVerbose (the more -v, the more verbose)"),
    OPT_STRING(OPT_POSITIONAL(1), NULL, tea_name, OPT_REQUIRED, ""),
    OPT_END
  }
};

int main(int argc, char **argv)
{
  opt_parse(&options, argv+1);
  return 0;
}

位置选项存在错误。如果有两个OPT_STRING,一个是位置的,一个不是,它无法解析。 - NewBee

2

如果我可以自夸一下的话,我想建议您看一下我写的一个选项解析库:dropt

  • 它是一个 C 库(如有需要,还带有 C++ 包装器)。
  • 它很轻量级。
  • 它是可扩展的(自定义参数类型可以很容易地添加,并且与内置参数类型平起平坐)。
  • 它应该非常易于移植(它是用标准 C 编写的),没有依赖项(除了 C 标准库)。
  • 它具有非常不受限制的许可证(zlib/libpng)。

它提供的一个许多其他库不具备的功能是覆盖先前选项的能力。例如,如果您有一个 shell 别名:

alias bar="foo --flag1 --flag2 --flag3"

如果你想使用bar但禁用--flag1,可以这样做:

bar --flag1=0

2

我写了一个类似于POpt的解析参数小型库,名为XOpt。由于我在使用POpt时遇到了一些问题,因此我开发了这个库。它采用GNU样式的参数解析,并具有与POpt非常相似的界面。

我经常使用它,并且取得了巨大的成功,它几乎可以在任何地方运行。


2
我写了一个叫做cmdparser的命令行解析库。它已经通过了完整测试,并支持嵌套子命令。以下是一个问题的示例:

https://github.com/XUJINKAI/cmdparser/

static cmdp_action_t callback(cmdp_process_param_st *params);
static bool g_line_by_line = false;
static bool g_word_by_word = false;
static bool g_case_insensitive = false;

static cmdp_command_st cmdp = {
    .options = {
        {'l', NULL, "line by line", CMDP_TYPE_BOOL, &g_line_by_line },
        {'w', NULL, "word by word", CMDP_TYPE_BOOL, &g_word_by_word },
        {'i', NULL, "case insensitive", CMDP_TYPE_BOOL, &g_case_insensitive },
        {0},
    },
    .fn_process = callback,
};

int main(int argc, char **argv) {
    return cmdp_run(argc - 1, argv + 1, &cmdp);
}

static cmdp_action_t callback(cmdp_process_param_st *params) {
    if (g_line_by_line && g_word_by_word) {
        return CMDP_ACT_FAIL | CMDP_ACT_SHOW_HELP;
    }

    // your code here...

    return CMDP_ACT_OVER;
}

0
#include <stdio.h>

int main(int argc, char **argv)
{
    size_t i;
    size_t filename_i = -1;

    for (i = 0; i < argc; i++)
    {
        char const *option =  argv[i];
        if (option[0] == '-')
        {
            printf("I am a flagged option");
            switch (option[1])
            {
                case 'a':
                    /*someting*/
                    break;
                case 'b':
                    break;
                case '-':
                    /* "--" -- the next argument will be a file.*/
                    filename_i = i;
                    i = i + 1;
                    break;
                default:
                    printf("flag not recognised %s", option);
                    break;
            }
        }
        else
        {   
            printf("I am a positional argument");
        }

        /* At this point, if -- was specified, then filename_i contains the index
         into argv that contains the filename. If -- was not specified, then filename_i will be -1*/
     }
  return 0;
}

5
不,这绝不是一个好的做法...使用其中一个参数解析函数 - getopt()getopt_long() - Jonathan Leffler
5
听起来像是作弊,因为这明显是一道家庭作业问题。此外,提问者很难理解字符串的概念以及如何读取其中的部分内容。强行让他使用"getopts"是一个错误。 - Pod
这是一个作业问题。我知道什么是字符串。但我不明白如何分解命令行参数,因为当你可以任意次输入选项时,它似乎很混乱,所以你无法确定文件名的位置。也许我想太多了? - user1251020

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接